CodeGym /Java блог /Случаен /Всичко за модулното тестване: техники, концепции, практик...
John Squirrels
Ниво
San Francisco

Всичко за модулното тестване: техники, концепции, практика

Публикувано в групата
Днес няма да намерите приложение, което да не е покрито с тестове, така че тази тема ще бъде по-актуална от всякога за начинаещите разработчици: не можете да успеете без тестове. Нека разгледаме Howви видове тестване се използват по принцип и след това ще проучим подробно всичко, което трябва да знаем за тестването на единици. Всичко за модулното тестване: техники, концепции, практика - 1

Видове тестове

Какво е тест? Според Wikipedia: „Софтуерното тестване включва изпълнението на софтуерен компонент or системен компонент за оценка на едно or повече свойства, представляващи интерес.“ С други думи, това е проверка на коректността на нашата система в определени ситуации. Е, нека да видим Howви видове тестове има като цяло:
  • Единично тестване — Тестове, чиято цел е да проверят всеки модул на системата поотделно. Тези тестове трябва да се прилагат за най-малките атомни части на системата, например модули.
  • Тестване на системата — Тестване на високо ниво за проверка на работата на по-голяма част от приложението or системата като цяло.
  • Регресионно тестване — Тестване, което се използва, за да се провери дали нови функции or корекции на грешки засягат съществуващата функционалност на приложението or въвеждат стари грешки.
  • Функционално тестване — Проверка дали част от приложението отговаря на изискванията, посочени в спецификациите, потребителските истории и др.

    Видове функционални тестове:

    • Тестване в бяла кутия — Проверка дали част от приложението отговаря на изискванията, като същевременно се знае вътрешната реализация на системата;
    • Тестване в черна кутия — Проверка дали част от приложението отговаря на изискванията, без да се знае вътрешната реализация на системата.

  • Тестване на производителността — Тестове, които са написани, за да определят How системата or част от системата работи при определено натоварване.
  • Тестване на натоварване — Тестове, предназначени да проверят стабилността на системата при стандартни натоварвания и да намерят максималното натоварване, при което приложението все още работи правилно.
  • Стрес тестване — Тестване, предназначено да провери производителността на приложението при нестандартни натоварвания и да определи максималното натоварване преди отказ на системата.
  • Тестване на сигурността — Тестове, използвани за проверка на сигурността на системата (от хакери, вируси, неоторизиран достъп до поверителни данни и други възхитителни атаки).
  • Тестване на локализация — Тестове на локализацията на приложението.
  • Тестване на използваемостта — Тестване, насочено към проверка на използваемостта, разбираемостта, привлекателността и възможността за обучение.
Всичко това звучи добре, но How работи на практика? просто! Използваме тестовата пирамида на Майк Кон: Всичко за модулното тестване: техники, концепции, практика - 2Това е опростена version на пирамидата: сега тя е разделена на още по-малки части. Но днес няма да ставаме твърде сложни. Ще разгледаме най-простата version.
  1. Unit — Този раздел се отнася за тестове на модули, които се прилагат в различни слоеве на приложението. Те тестват най-малката делима единица на логиката на приложението. Например класове, но най-често методи. Тези тестове обикновено се опитват колкото е възможно повече да изолират това, което се тества от всяка външна логика. Тоест те се опитват да създадат илюзията, че останалата част от приложението работи според очакванията.

    Винаги трябва да има много от тези тестове (повече от всеки друг тип), тъй като те тестват малки парчета и са много леки, не консумират много ресурси (което означава RAM и време).

  2. Интегриране — Този раздел се отнася за интеграционно тестване. Това тестване проверява по-големи части от системата. Тоест, той or комбинира няколко части от логиката (няколко метода or класа), or проверява коректността на взаимодействие с външен компонент. Тези тестове обикновено са по-малки от единичните тестове, защото са по-тежки.

    Пример за интеграционен тест може да бъде свързване към база данни и проверка на правилността на работата на методите за работа с нея.

  3. UI — Този раздел се отнася за тестове, които проверяват работата на потребителския интерфейс. Те включват логиката на всички нива на приложението, поради което се наричат ​​още тестове от край до край. По правило те са много по-малко, защото са най-тромави и трябва да проверяват най-необходимите (използваните) пътеки.

    На снимката по-горе виждаме, че различните части на триъгълника варират по размер: приблизително същите пропорции съществуват в броя на различните видове тестове в реална работа.

    Днес ще разгледаме по-подробно най-често срещаните тестове, unit tests, тъй като всички уважаващи себе си Java разработчици трябва да могат да ги използват на основно ниво.

Ключови понятия в модулното тестване

Тестовото покритие (покритието на codeа) е една от основните мярки за това колко добре е тествано дадено приложение. Това е процентът на codeа, който е покрит от тестовете (0-100%). На практика мнозина преследват този процент като своя цел. Това е нещо, с което не съм съгласен, тъй като това означава, че тестовете започват да се прилагат там, където не са необходими. Да предположим например, че имаме стандартни CRUD (създаване/получаване/актуализиране/изтриване) операции в нашата услуга без допълнителна логика. Тези методи са чисто посредници, които делегират работа на слоя, работещ с хранorщето. В тази ситуация няма Howво да тестваме, освен може би дали дадения метод извиква DAO метод, но това е шега. Обикновено се използват допълнителни инструменти за оценка на покритието на теста: JaCoCo, Cobertura, Clover, Emma и др. За по-подробно проучване на тази тема, TDD означава разработка, управлявана от тестове. При този подход, преди да направите нещо друго, пишете тест, който ще провери конкретен code. Това се оказва тестване на черна кутия: знаем, че входът е и знаем Howъв трябва да бъде изходът. Това позволява да се избегне дублиране на code. Разработката, управлявана от тестове, започва с проектиране и разработване на тестове за всяка част от функционалността във вашето приложение. При подхода TDD първо създаваме тест, който дефинира и тества поведението на codeа. Основната цел на TDD е да направи вашия code по-разбираем, по-опростен и без грешки. Всичко за модулното тестване: техники, концепции, практика - 3Подходът се състои в следното:
  • Пишем нашия тест.
  • Провеждаме теста. Не е изненадващо, че се проваля, тъй като все още не сме внедрor необходимата логика.
  • Добавете codeа, който кара теста да премине (пускаме теста отново).
  • Ние преработваме codeа.
TDD се основава на единични тестове, тъй като те са най-малките градивни елементи в пирамидата за автоматизация на тестовете. С модулни тестове можем да тестваме бизнес логиката на всеки клас. BDD означава развитие, управлявано от поведението. Този подход се основава на TDD. По-конкретно, той използва примери на обикновен език, които обясняват поведението на системата за всеки, участващ в разработката. Няма да се задълбочаваме в този термин, тъй като той засяга основно тестери и бизнес анализатори. Тестовият случай е сценарий, който описва стъпките, специфичните условия и параметрите, необходими за проверка на тествания code. Тестовото устройство е code, който настройва тестовата среда да има състоянието, необходимо за успешното изпълнение на тествания метод. Това е предварително дефиниран набор от обекти и тяхното поведение при определени условия.

Етапи на тестване

Тестът се състои от три етапа:
  • Посочете тестови данни (приспособления).
  • Упражнявайте тествания code (извикайте тествания метод).
  • Проверете резултатите и ги сравнете с очакваните резултати.
Всичко за модулното тестване: техники, концепции, практика - 4За да осигурите модулност на теста, трябва да се изолирате от другите слоеве на приложението. Това може да се направи с помощта на мъничета, мокове и шпиони. Макетите са обекти, които могат да бъдат персонализирани (например пригодени за всеки тест). Те ни позволяват да уточним Howво очакваме от извикванията на метода, т.е. очакваните отговори. Използваме фалшиви обекти, за да проверим дали получаваме това, което очакваме. Плъзгачите осигуряват твърдо codeиран отговор на повиквания по време на тестване. Те могат също да съхраняват информация за повикването (например параметри or брой повиквания). Понякога ги наричат ​​шпиони. Понякога хората бъркат термините мъниче и макет: разликата е, че мъничето не проверява нищо — то само симулира дадено състояние. Макетът е обект, който има очаквания. Например, че даден метод трябва да бъде извикан определен брой пъти. С други думи,

Тестови среди

И така, сега към точката. Има няколко тестови среди (рамки), налични за Java. Най-популярните от тях са JUnit и TestNG. За нашия преглед тук използваме: Всичко за модулното тестване: техники, концепции, практика - 5JUnit тест е метод в клас, който се използва само за тестване. Класът обикновено се нарича по същия начин като класа, който тества, с добавено „Тест“ в края. Например CarService -> CarServiceTest. Системата за изграждане на Maven автоматично включва такива класове в тестовия обхват. Всъщност този клас се нарича тестов клас. Нека прегледаме накратко основните анотации:

  • @Test показва, че методът е тест (по принцип метод, маркиран с тази анотация, е единичен тест).
  • @Before означава метод, който ще се изпълнява преди всеки тест. Например, за да попълните клас с тестови данни, да прочетете входни данни и т.н.
  • @After се използва за маркиране на метод, който ще бъде извикан след всеки тест (напр. за изчистване на данни or възстановяване на стойностите по подразбиране).
  • @BeforeClass се поставя над метод, аналогичен на @Before. Но такъв метод се извиква само веднъж преди всички тестове за дадения клас и следователно трябва да бъде статичен. Използва се за извършване на операции, изискващи повече ресурси, като например завъртане на тестова база данни.
  • @AfterClass е обратното на @BeforeClass: изпълнява се веднъж за дадения клас, но само след всички тестове. Използва се например за изчистване на постоянни ресурси or прекъсване на връзката с база данни.
  • @Ignore означава, че даден метод е деактивиран и ще бъде игнориран по време на цялостното тестово изпълнение. Това се използва в различни ситуации, например, ако основният метод е променен и тестът все още не е преработен, за да приспособи промените. В такива случаи също е желателно да добавите описание, т.е. @Ignore("НяHowво описание").
  • @Test(expected = Exception.class) се използва за отрицателни тестове. Това са тестове, които проверяват How се държи методът в случай на грешка, тоест тестът очаква методът да хвърли няHowъв вид изключение. Такъв метод се обозначава с анотацията @Test, но с указание коя грешка да се хване.
  • @Test(timeout = 100) проверява дали методът се изпълнява за не повече от 100 мorсекунди.
  • @Mock се използва над поле за присвояване на макет обект (това не е JUnit анотация, а instead of това идва от Mockito). Ако е необходимо, задаваме поведението на макета за конкретна ситуация директно в тестовия метод.
  • @RunWith(MockitoJUnitRunner.class) се поставя над клас. Тази анотация казва на JUnit да извика тестовете в класа. Има различни бегачи, включително тези: MockitoJUnitRunner, JUnitPlatform и SpringRunner. В JUnit 5 анотацията @RunWith е заменена с по-мощната анотация @ExtendWith.
Нека да разгледаме някои методи, използвани за сравняване на резултатите:

  • assertEquals(Object expects, Object actuals) — проверява дали предадените обекти са равни.
  • assertTrue(булев флаг) — проверява дали предадената стойност е истина.
  • assertFalse(булев флаг) — проверява дали предадената стойност е false.
  • assertNull(Object object) — проверява дали предаваният обект е null.
  • assertSame(Object firstObject, Object secondObject) — проверява дали предадените стойности се отнасят за същия обект.
  • assertThat(T t, Съпоставяне съвпадение) — Проверява дали t удовлетворява conditionто, посочено в matcher.
AssertJ също предоставя полезен метод за сравнение: assertThat(firstObject).isEqualTo(secondObject) . Тук споменах основните методи - другите са вариации на горните.

Тестване на практика

Сега нека разгледаме горния материал в конкретен пример. Ще тестваме метод за актуализиране на услуга. Няма да разглеждаме DAO слоя, тъй като използваме стандартния. Нека добавим стартер за тестовете:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
И тук имаме класа на обслужване:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
Ред 8 — изтеглете актуализирания обект от базата данни. Редове 9-14 — създаване на обект чрез строителя. Ако входящият обект има поле, задайте го. Ако не, ще оставим това, което е в базата данни. Сега вижте нашия тест:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
Ред 1 — нашият бегач. Ред 4 — ние изолираме услугата от DAO слоя, като заместваме макет. Ред 11 — задаваме тестов обект (този, който ще използваме като морско зайче) за класа. Ред 22 — задаваме сервизния обект, който ще тестваме.

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Тук виждаме, че тестът има три ясни подразделения: Редове 3-9 — определяне на тела. Ред 11 — изпълнение на тествания code. Редове 13-17 — проверка на резултатите. По-подробно: Редове 3-4 — задайте поведението за макета на DAO. Ред 5 — задайте екземпляра, който ще актуализираме над нашия стандарт. Ред 11 — използвайте метода и вземете получения екземпляр. Ред 13 — проверете дали не е нула. Ред 14 — сравнете идентификатора на резултата и дадените аргументи на метода. Ред 15 — проверете дали името е актуализирано. Ред 16 — вижте резултата от процесора. Ред 17 — не сме посочor това поле в екземпляра, така че трябва да остане същото. Проверяваме това condition тук. Нека го стартираме:Всичко за модулното тестване: техники, концепции, практика - 6Тестът е зелен! Можем да въздъхнем с облекчение :) В обобщение, тестването подобрява качеството на codeа и прави процеса на разработка по-гъвкав и надежден. Представете си колко усorя са необходими за редизайн на софтуер, включващ стотици клас файлове. Когато имаме модулни тестове, написани за всички тези класове, можем да рефакторираме с увереност. И най-важното, помага ни лесно да намираме грешки по време на разработката. Момчета и момичета, това е всичко, което имам днес. Дайте ми лайк и оставете коментар :)
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION