Автоматизираните софтуерни тестове са от решаващо значение за дългосрочното качество, поддръжката и разширяемостта на софтуерните проекти, а за Java JUnit е пътят към автоматизацията.
Докато по-голямата част от тази статия ще се съсредоточи върху писането на стабилни модулни тестове и използване на заглушаване, подигравки и инжектиране на зависимости, ние също ще обсъдим JUnit и тестове за интеграция.
The JUnit тест рамка е често срещан, безплатен и инструмент с отворен код за тестване на базирани на Java проекти.
Към момента на писане, JUnit 4 е настоящата основна версия, издадена преди повече от 10 години, като последната актуализация е била преди повече от две години.
JUnit 5 (с моделите за програмиране и разширение на Юпитер) е в активно развитие. Той по-добре поддържа езикови функции, въведени в Java 8 и включва други нови, интересни функции. Някои екипи могат да намерят JUnit 5 готов за употреба, докато други могат да продължат да използват JUnit 4, докато официално не бъде пуснат 5. Ще разгледаме примери и от двете.
Тестовете JUnit могат да се изпълняват директно в IntelliJ, но могат да се изпълняват и в други IDE като Eclipse, NetBeans или дори командния ред.
Тестовете винаги трябва да се изпълняват по време на изграждане, особено единични тестове. Компилация с неуспешни тестове трябва да се счита за неуспешна, независимо дали проблемът е в производствения или тестовия код - това изисква дисциплина от екипа и готовност да се даде най-голям приоритет на разрешаването на неуспешни тестове, но е необходимо да се придържате към дух на автоматизация.
хакване на кредитна карта с валидно cvv
Тестовете JUnit също могат да се изпълняват и отчитат от системи за непрекъсната интеграция като Jenkins. Проектите, които използват инструменти като Gradle, Maven или Ant, имат допълнителното предимство, че могат да изпълняват тестове като част от процеса на изграждане.
Като примерен проект Gradle за JUnit 5, вижте Раздел Gradle на ръководството за потребителя на JUnit и junit5-sample.git хранилище. Имайте предвид, че може да изпълнява и тестове, които използват API на JUnit 4 (посочен като „Реколта“ ).
Проектът може да бъде създаден в IntelliJ чрез опцията от менюто Файл> Отваряне ...> отидете до junit-gradle-consumer sub-directory
> ОК> Отваряне като проект> ОК, за да импортирате проекта от Gradle.
За Eclipse, Приставка Buildle Gradle може да бъде инсталиран от Помощ> Eclipse Marketplace ... След това проектът може да бъде импортиран с File> Import ...> Gradle> Gradle Project> Next> Next> Преглед на junit-gradle-consumer
поддиректория> Напред> Напред> Готово.
След настройване на проекта Gradle в IntelliJ или Eclipse, стартиране на Gradle build
задачата ще включва провеждането на всички тестове JUnit с test
задача. Имайте предвид, че тестовете могат да бъдат пропуснати при последващи изпълнения на build
ако не са направени промени в кода.
За JUnit 4 вижте JUnit’s използвайте с уикито Gradle .
За JUnit 5 вижте Раздел Maven на ръководството за потребителя и junit5-sample.git хранилище за пример за проект на Maven. Това може да стартира и ретро тестове (такива, които използват JUnit 4 API).
В IntelliJ използвайте File> Open ...> отидете до junit-maven-consumer/pom.xml
> ОК> Отваряне като проект. След това тестовете могат да се стартират от Maven Projects> junit5-maven-consumer> Lifecycle> Test.
В Eclipse използвайте Файл> Импортиране ...> Maven> Съществуващи проекти на Maven> Напред> Преглед на junit-maven-consumer
директория> С pom.xml
избрано> Край.
Тестовете могат да бъдат изпълнени чрез стартиране на проекта като Maven build ...> посочете целта на test
> Пусни.
За JUnit 4 вижте JUnit в хранилището на Maven .
В допълнение към провеждането на тестове чрез инструменти за изграждане като Gradle или Maven, много IDE могат директно да изпълняват тестове JUnit.
IntelliJ IDEA 2016.2 или по-нова версия е необходима за тестове JUnit 5, докато тестовете JUnit 4 трябва да работят в по-стари версии на IntelliJ.
За целите на тази статия може да искате да създадете нов проект в IntelliJ от едно от моите хранилища на GitHub ( JUnit5IntelliJ.git или JUnit4IntelliJ.git ), които включват всички файлове в прости Person
пример за клас и използвайте вградените библиотеки JUnit. Тестът може да се изпълни с Run> Run ‘All Tests’. Тестът може да се изпълни и в IntelliJ от PersonTest
клас.
Тези хранилища са създадени с нови проекти на IntelliJ Java и изграждат структурите на директории src/main/java/com/example
и src/test/java/com/example
. src/main/java
директория е посочена като изходна папка, докато src/test/java
е посочен като папка за тестов източник. След създаване на PersonTest
клас с тестов метод, коментиран с @Test
, той може да не успее да се компилира, като в този случай IntelliJ предлага предложението да добавите JUnit 4 или JUnit 5 към пътя на класа, който може да бъде зареден от IntelliJ IDEA дистрибуцията (вж. тези отговори за препълване на стека за повече подробности). И накрая, за всички тестове беше добавена конфигурация за изпълнение на JUnit.
Вижте също Насоки за тестване на IntelliJ .
Празно Java проектът в Eclipse няма да има тестова коренна директория. Това е добавено от Свойства на проекта> Път за изграждане на Java> Добавяне на папка ...> Създаване на нова папка ...> посочете името на папката> Готово. Новата директория ще бъде избрана като изходна папка. Щракнете върху OK в двата останали диалога.
Тестовете на JUnit 4 могат да бъдат създадени с File> New> JUnit Test Case. Изберете „New JUnit 4 test“ и новосъздадената папка източник за тестове. Посочете „тестван клас“ и „пакет“, като се уверите, че пакетът съответства на тествания клас. След това посочете име за тестовия клас. След приключване на съветника, ако бъдете подканени, изберете „Добавяне на библиотека JUnit 4“ към пътя на изграждане. След това проектът или индивидуалният тест може да се изпълни като JUnit тест. Вижте също Eclipse Writing и стартиране на JUnit тестове .
NetBeans поддържа само тестове JUnit 4. Тестови класове могат да бъдат създадени в проект на Java на NetBeans с File> New File ...> Unit Tests> JUnit Test или Test for Existing Class. По подразбиране тестовата коренна директория е наречена test
в директорията на проекта.
Нека да разгледаме прост пример за производствен код и съответния код за единичен тест за много прост Person
клас. Можете да изтеглите примерния код от моя github проект и го отворете чрез IntelliJ.
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ', ' + givenName; } }
The неизменен Person
class има конструктор и getDisplayName()
метод. Искаме да тестваме това getDisplayName()
връща името, форматирано както очакваме. Ето кода на теста за единичен тест (JUnit 5):
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person('Josh', 'Hayden'); String displayName = person.getDisplayName(); assertEquals('Hayden, Josh', displayName); } }
PersonTest
използва JUnit 5’s @Test
и твърдение. За JUnit 4, PersonTest
класът и методът трябва да са публични и да се използва различен внос. Ето това JUnit 4 пример Gist .
При стартиране на PersonTest
клас в IntelliJ, тестът преминава и индикаторите на потребителския интерфейс са зелени.
Въпреки че не е задължително, ние използваме общи конвенции при именуване на тестовия клас; по-конкретно, започваме с името на тествания клас (Person
) и добавяме „Test“ към него (PersonTest
). Именуването на метода за изпитване е подобно, като се започне с метода, който се тества (getDisplayName()
) и се добави 'test' към него (testGetDisplayName()
). Въпреки че има много други напълно приемливи конвенции за именуване на методи за тестване, важно е да бъдете последователни в екипа и проекта.
Име в производство | Име в Тестване |
---|---|
Личност | Тест за лице |
getDisplayName() | testDisplayName() |
Ние също използваме конвенцията за създаване на тестов код PersonTest
клас в същия пакет (com.example
) като производствения код Person
клас. Ако използвахме различен пакет за тестове, ще трябва да използваме публиката достъп редактиране в производствени кодове класове, конструктори и методи, посочени от модулни тестове, дори когато това не е подходящо, така че е по-добре просто да ги съхранявате в един и същи пакет. Ние обаче използваме отделни директории на източника (src/main/java
и src/test/java
), тъй като обикновено не искаме да включваме тестов код в издадени производствени компилации.
@Test
анотация (JUnit 4 / 5 ) казва на JUnit да изпълни testGetDisplayName()
метод като метод за изпитване и докладвайте дали преминава или не. Докато всички твърдения (ако има такива) преминат и не се хвърлят изключения, тестът се счита за успешен.
как да създадете резервен продукт
Нашият тестов код следва структурния модел на Подреждане-действие-утвърждаване (AAA) . Други често срещани модели включват Give-When-Then и Setup-Exercise-Verify-Teardown (Teardown обикновено не е изрично необходим за единични тестове), но ние използваме AAA в тази статия.
Нека да разгледаме как нашият тестов пример следва AAA. Първият ред, „подредете“ създава Person
обект, който ще бъде тестван:
Person person = new Person('Josh', 'Hayden');
Вторият ред, 'акт', упражнения производствения код’s Person.getDisplayName()
метод:
String displayName = person.getDisplayName();
Третият ред, „отстояване“, проверява дали резултатът е според очакванията.
assertEquals('Hayden, Josh', displayName);
Вътрешно assertEquals()
call използва метода на еквивалента на обекта String „Hayden, Josh“, за да провери действителната стойност, върната от съвпаденията на производствения код (displayName
). Ако не съвпадаше, тестът щеше да бъде означен като неуспешен.
Имайте предвид, че тестовете често имат повече от един ред за всяка от тези фази AAA.
След като разгледахме някои конвенции за тестване, нека насочим вниманието си към това да направим производствения код проверяваем.
Връщаме се към нашите Person
клас, където съм внедрил метод за връщане на възрастта на човек въз основа на неговата дата на раждане. Примерите за код изискват Java 8 да се възползва от новите API за дата и функционалност. Ето какво е новото Person.java
клас изглежда така:
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
Изпълнението на този клас (по време на писането) обявява, че Джоуи е на 4 години. Нека добавим метод за тест:
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); long age = person.getAge(); assertEquals(4, age); } }
Отминава днес, но какво ще кажете, когато го стартираме след една година? Този тест е недетерминиран и чуплив, тъй като очакваният резултат зависи от текущата дата на системата, изпълняваща теста.
Когато работим в производството, ние искаме да използваме текущата дата, LocalDate.now()
, за изчисляване на възрастта на човека, но за да направим детерминиран тест дори след една година, тестовете трябва да предоставят свои собствени currentDate
стойности.
Това е известно като инжектиране на зависимост. Не искаме нашите Person
обект, за да определи самата текуща дата, но вместо това искаме да предадем тази логика като зависимост. Единичните тестове ще използват известна, намалена стойност, а производственият код ще позволи действителната стойност да бъде предоставена от системата по време на изпълнение.
Нека добавим LocalDate
доставчик на Person.java
:
aws сертифицирани решения архитект сътрудник учебно ръководство
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person('Joey', 'Doe', LocalDate.parse('2013-01-12')); System.out.println(person.getDisplayName() + ': ' + person.getAge() + ' years'); // Doe, Joey: 4 years } }
За да улесните тестването на getAge()
метод, ние го променихме, за да използва currentDateSupplier
, a LocalDate
доставчик, за извличане на текущата дата. Ако не знаете какво е доставчик, препоръчвам да прочетете Ламбда вградени функционални интерфейси .
Добавихме и инжекция на зависимост: Новият конструктор за тестване позволява на тестовете да предоставят свои собствени текущи стойности на датата. Оригиналният конструктор извиква този нов конструктор, като предава статичен референтен метод на LocalDate::now
, който предоставя LocalDate
обект, така че основният ни метод все още работи както преди. Ами нашият метод за тестване? Нека актуализираме PersonTest.java
:
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse('2013-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-17'); Person person = new Person('Joey', 'Doe', dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Тестът сега инжектира свой собствен currentDate
стойност, така че нашият тест все пак ще премине, когато бъде стартиран през следващата година или през която и да е година. Това обикновено се нарича стърчащ , или предоставяне на известна стойност, която да бъде върната, но първо трябваше да променим Person
за да се даде възможност за инжектиране на тази зависимост.
Обърнете внимание на ламбда синтаксис (()->currentDate
) при конструиране на Person
обект. Това се третира като доставчик на LocalDate
, както се изисква от новия конструктор.
Готови сме за нашите Person
обект - чието цялостно съществуване е в паметта на JVM - да комуникира с външния свят. Искаме да добавим два метода: publishAge()
метод, който ще публикува текущата възраст на човека и getThoseInCommon()
метод, който ще върне имена на известни хора, които споделят същия рожден ден или са на същата възраст като нашата Person
. Да предположим, че има услуга RESTful, с която можем да взаимодействаме, наречена „Рождени дни на хората“. Имаме клиент на Java за него, който се състои от един клас, BirthdaysClient
.
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println('publishing ' + name + ''s age: ' + age); // HTTP POST with name and age and possibly throw an exception } public Collection findFamousNamesOfAge(long age) throws IOException { System.out.println('finding famous names of age ' + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println('finding famous names born on day ' + dayOfMonth + ' of month ' + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Нека подобрим нашите Person
клас. Започваме с добавяне на нов метод за тестване за желаното поведение на publishAge()
. Защо да започнем с теста, а не с функционалността? Ние следваме принципите на разработеното с тест (познато още като TDD), при което първо пишем теста, а след това кода, за да го направим успешно.
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate); person.publishAge(); } }
На този етап тестовият код не успява да се компилира, тъй като не сме създали publishAge()
метод, който извиква. След като създадем празен Person.publishAge()
метод, всичко минава. Вече сме готови за теста, за да потвърдим, че възрастта на човека действително се публикува в BirthdaysClient
Тъй като това е единичен тест, той трябва да работи бързо и в памет, така че тестът ще изгради нашите Person
обект с макет BirthdaysClient
така че всъщност не прави уеб заявка. След това тестът ще използва този фалшив обект, за да провери дали е извикан според очакванията. За да направите това, ще добавим зависимост от Mockito рамка (MIT лиценз) за създаване на фиктивни обекти и след това създаване на подигравани BirthdaysClient
обект:
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Освен това увеличихме подписа на Person
конструктор, за да вземе BirthdaysClient
обект и смени теста, за да инжектира подигравания BirthdaysClient
обект.
След това добавяме в края на нашите testPublishAge
очакване, че BirthdaysClient
е наречен. Person.publishAge()
трябва да го извика, както е показано в новия ни PersonTest.java
:
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); } }
Нашите Mockito-подобрени BirthdaysClient
проследява всички обаждания, които са били извършени към неговите методи, по този начин ние проверяваме, че не са извършени обаждания до BirthdaysClient
с verifyZeroInteractions()
метод преди извикване publishAge()
. Въпреки че може би не е необходимо, като правим това, ние гарантираме, че конструкторът не прави никакви измамници. На verify()
линия, ние уточняваме как очакваме повикването до BirthdaysClient
да гледам.
Имайте предвид, че тъй като objavRegularPersonAge има IOException в своя подпис, ние го добавяме и към нашия тест метод за подпис.
В този момент тестът се проваля:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( 'Joe Sixteen', 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Това се очаква, като се има предвид, че все още не сме внедрили необходимите промени в Person.java
, тъй като следваме тестово развитие. Сега ще направим този тест, като направим необходимите промени:
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + ' ' + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
Направихме конструктора на производствен код да създаде нов BirthdaysClient
и publishAge()
сега извиква birthdaysClient
. Всички тестове преминават; всичко е зелено. Страхотен! Но забележете, че publishAge()
поглъща IOException. Вместо да го оставим да се издуе, ние искаме да го обгърнем със собствения си PersonException в нов файл, наречен PersonException.java
:
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Ние прилагаме този сценарий като нов метод за тестване в PersonTest.java
:
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge('Joe Sixteen', 16); try { person.publishAge(); fail('expected exception not thrown'); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals('Failed to publish Joe Sixteen age 16', e.getMessage()); } } }
The Mockito doThrow()
обаждания birthdaysClient
да се хвърли изключение, когато publishRegularPersonAge()
метод се извиква. Ако PersonException
не е хвърлен, ние се проваляме на теста. В противен случай твърдим, че изключението е било правилно окован с IOException и проверете дали съобщението за изключение е както се очаква. В момента, тъй като не сме внедрили никакви манипулации в нашия производствен код, тестът ни се проваля, тъй като очакваното изключение не беше хвърлено. Ето какво трябва да променим в Person.java
за да премине теста:
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException('Failed to publish ' + nameToPublish + ' age ' + age, e); } } }
Сега изпълняваме Person.getThoseInCommon()
метод, като правим Person.Java
клас изглежда това .
Нашият testGetThoseInCommon()
, за разлика от testPublishAge()
, не потвърждава, че са извършени конкретни повиквания до birthdaysClient
методи. Вместо това използва when
извиква стойности за връщане на стойки за повиквания към findFamousNamesOfAge()
и findFamousNamesBornOn()
че getThoseInCommon()
ще трябва да направи. След това твърдим, че и трите посочени имена, които сме предоставили, се връщат.
Опаковане на множество твърдения с assertAll()
Методът JUnit 5 позволява да се проверяват всички твърдения като цяло, вместо да се спира след първото неуспешно твърдение. Включваме и съобщение с assertTrue()
за идентифициране на конкретни имена, които не са включени. Ето как изглежда нашият метод за тестване на „щастлив път“ (идеален сценарий) (имайте предвид, че това не е стабилен набор от тестове, които по своята същност са „щастлив път“, но за това ще говорим по-късно.
Извадка от проектен документ на високо ниво
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse('2000-01-02'); LocalDate currentDate = LocalDate.parse('2017-01-01'); Person person = new Person('Joe', 'Sixteen', dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList('JoeFamous Sixteen', 'Another Person')); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList('Jan TwoKnown')); Set thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, 'Another Person'), setContains(thoseInCommon, 'Jan TwoKnown'), setContains(thoseInCommon, 'JoeFamous Sixteen'), ()-> assertEquals(3, thoseInCommon.size()) ); } private Executable setContains(Set set, T expected) { return () -> assertTrue(set.contains(expected), 'Should contain ' + expected); } // ... }
Макар често да се пренебрегва, е също толкова важно да запазите тестовия код без гниещо дублиране. Чист код и принципи като 'Не се повтаряйте' са много важни за поддържането на висококачествена кодова база, производствен и тестов код. Забележете, че най-новият PersonTest.java има известно дублиране сега, когато имаме няколко метода за тестване.
За да поправим това, можем да направим няколко неща:
Извлечете обекта IOException в частно крайно поле.
Извличане на Person
създаване на обект в собствен метод (createJoeSixteenJan2()
, в този случай), тъй като повечето обекти на Person се създават със същите параметри.
Създайте assertCauseAndMessage()
за различните тестове, които проверяват хвърлените PersonExceptions
.
Резултатите от чистия код могат да се видят в това предаване на PersonTest.java файл.
Какво да правим, когато a Person
обектът има дата на раждане, която е по-късна от текущата дата? Дефектите в приложенията често се дължат на неочаквано въвеждане или липса на предвидливост в ъглови, ръбни или гранични случаи. Важно е да се опитаме да предвидим тези ситуации възможно най-добре, а модулните тестове често са подходящо място за това. При изграждането на нашите Person
и PersonTest
, включихме няколко теста за очаквани изключения, но в никакъв случай не беше пълен. Например използваме LocalDate
което не представлява или съхранява данни за часовата зона. Извикванията ни към LocalDate.now()
обаче връщат LocalDate
въз основа на часовата зона по подразбиране на системата, която може да е ден по-рано или по-късно от тази на потребителя на системата. Тези фактори трябва да се вземат предвид с подходящи тестове и поведение.
Границите също трябва да бъдат тествани. Помислете за Person
обект с getDaysUntilBirthday()
метод. Тестването трябва да включва дали рожденият ден на човека вече е преминал през текущата година, дали рожденият ден на човека е днес и как високосна година влияе върху броя на дните. Тези сценарии могат да бъдат покрити чрез проверка един ден преди рождения ден на човека, деня и един ден след рождения ден на човека, където следващата година е високосна. Ето съответния тестов код:
// ... class PersonTest { private final Supplier currentDateSupplier = ()-> LocalDate.parse('2015-05-02'); private final LocalDate ageJustOver5 = LocalDate.parse('2010-05-01'); private final LocalDate ageExactly5 = LocalDate.parse('2010-05-02'); private final LocalDate ageAlmost5 = LocalDate.parse('2010-05-03'); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function personLongFunction) { Person person = new Person('Given', 'Sur', dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Основно сме се фокусирали върху модулни тестове, но JUnit може да се използва и за интеграция, приемане, функционални и системни тестове. Такива тестове често изискват повече код за настройка, напр. Стартиране на сървъри, зареждане на бази данни с известни данни и др. Въпреки че често можем да стартираме хиляди единични тестове за секунди, големи тестови пакети за интеграция могат да отнемат минути или дори часове. Тестовете за интеграция обикновено не трябва да се използват, за да се опитат да обхванат всяка пермутация или път през кода; единичните тестове са по-подходящи за това.
Създаването на тестове за уеб приложения, които управляват уеб браузъри при попълване на формуляри, щракване върху бутони, изчакване на зареждане на съдържание и т.н., обикновено се извършва с помощта на Селен WebDriver (Лиценз за Apache 2.0), съчетан с „Шаблон на обект на страница“ (вижте Уики за селенHQ github и Статията на Мартин Фаулър за Page Objects ).
JUnit е ефективен за тестване на RESTful API с използване на HTTP клиент като Apache HTTP клиент или Spring Rest Template ( HowToDoInJava.com дава добър пример ).
В нашия случай с Person
обект, тест за интеграция може да включва използване на реалния BirthdaysClient
а не фалшив, с конфигурация, указваща основния URL адрес на услугата People Birthdays. Тогава интеграционният тест ще използва тестов екземпляр на такава услуга, ще провери дали рождените дни са публикувани в нея и ще създаде известни хора в услугата, които ще бъдат върнати.
JUnit има много допълнителни функции, които все още не сме изследвали в примерите. Ще опишем някои и ще предоставим препоръки за други.
Трябва да се отбележи, че JUnit създава нов екземпляр на тестовия клас за изпълнение на всеки @Test
метод. JUnit също така предоставя куки за анотиране, за да стартира определени методи преди или след всички или всеки от @Test
методи. Тези куки често се използват за настройване или почистване на база данни или фалшиви обекти и се различават между JUnit 4 и 5.
JUnit 4 | JUnit 5 | За статичен метод? |
---|---|---|
@BeforeClass | @BeforeAll | Да |
@AfterClass | @AfterAll | Да |
@Before | @BeforeEach | Не |
@After | @AfterEach | Не |
В нашия PersonTest
например избрахме да конфигурираме BirthdaysClient
макет на обекта в @Test
самите методи, но понякога трябва да бъдат изградени по-сложни фалшиви структури, включващи множество обекти. @BeforeEach
(в JUnit 5) и @Before
(в JUnit 4) често е подходящо за това.
@After*
анотациите са по-често срещани при интеграционните тестове, отколкото модулните тестове, тъй като JVM събирането на боклука обработва повечето обекти, създадени за модулни тестове. @BeforeClass
и @BeforeAll
анотациите се използват най-често за интеграционни тестове, които трябва да извършват скъпи действия за настройка и прекъсване веднъж, а не за всеки метод на тестване.
За JUnit 4, моля, обърнете се към ръководство за тестови тела (общите понятия все още се прилагат за JUnit 5).
Понякога искате да стартирате множество свързани тестове, но не всички тестове. В този случай групи от тестове могат да бъдат съставени в тестови пакети. За това как да направите това в JUnit 5, вижте Статията JUnit 5 на HowToProgram.xyz и в екипа на JUnit документация за JUnit 4 .
JUnit 5 добавя възможността да се използват нестатични вложени вътрешни класове, за да се покаже по-добре връзката между тестовете. Това трябва да е много познато на тези, които са работили с вложени описания в тестови рамки като Jasmine за JavaScript. Вътрешните класове са отбелязани с @Nested
за да използвате това.
@DisplayName
анотацията също е нова за JUnit 5, което ви позволява да опишете теста за отчитане в низ формат, който да бъде показан в допълнение към идентификатора на метода на теста.
Въпреки че @Nested
и @DisplayName
могат да се използват независимо един от друг, като заедно могат да осигурят по-ясни резултати от теста, които описват поведението на системата.
The Рамка на Hamcrest макар и да не е част от кодовата база на JUnit, предоставя алтернатива на използването на традиционни методи за утвърждаване в тестовете, позволявайки по-изразителен и четим тестов код. Вижте следната проверка, използвайки както традиционните assertEquals, така и Hamcrest, които:
//Traditional assert assertEquals('Hayden, Josh', displayName); //Hamcrest assert assertThat(displayName, equalTo('Hayden, Josh'));
Hamcrest може да се използва както с JUnit 4, така и с 5. Урок на Vogella.com за Hamcrest е доста изчерпателна.
Статията Единични тестове, как да напишем тестваем код и защо това е важно обхваща по-конкретни примери за писане на чист, проверяем код.
Изграждане с увереност: Ръководство за JUnit тестове разглежда различни подходи за модулно и интеграционно тестване и защо е най-добре да изберете един и да се придържате към него
заглавен файл в C++
The JUnit 4 Wiki и Ръководство за потребителя на JUnit 5 винаги са отлична отправна точка.
The Mockito документация предоставя информация за допълнителна функционалност и примери.
Проучихме много аспекти на тестването в света на Java с JUnit. Разгледахме модулни и интеграционни тестове, използващи JUnit рамката за Java кодови бази, интегриране на JUnit в среди за разработка и изграждане, как да използваме макети и мъничета с доставчици и Mockito, общи конвенции и най-добри кодови практики, какво да тестваме и някои други страхотни функции на JUnit.
Сега е ред на читателя да нарасне в умелото прилагане, поддържане и извличане на предимствата на автоматизираните тестове с помощта на JUnit рамката.