Skip to main content

Софтуерно тестване с JUnit - тестване на кода

Видове Тестове в Софтуерното Инженерство

  • Unit Testing: Тества най-малките части на приложението независимо (например, функции или методи).
  • Integration Testing: Проверява взаимодействията между различни модули или външни системи.
  • Functional Testing: Фокусира се върху бизнес изискванията и функционалността на софтуера.
  • End-to-End Testing: Симулира потребителски сценарии и тества целия процес от начало до край.
  • Performance Testing: Измерва производителността на приложението при различни условия.
  • Security Testing: Проверява за уязвимости в софтуера и сигурността на данните.
  • UI Testing: Тества потребителския интерфейс на приложението.

Unit Testing в Java с JUnit

Unit тестовете са основен инструмент за осигуряване на качеството на софтуера. Те позволяват на разработчиците да проверяват функционалността на отделни компоненти в изолация. В Java, най-използваната библиотека за unit тестове е JUnit.

Защо са Важни Unit Тестовете

  1. Ранно Откриване на Грешки: Помагат за откриване на проблеми в кода в ранен стадий.
  2. Документация: Предоставят практически примери за начина на използване на кода.
  3. Поддръжка и Рефакторинг: Улесняват рефакторинга на кода, като осигуряват увереност, че промените не нарушават съществуващата функционалност.

JUnit: Основни Концепции и Употреба

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

Компоненти на JUnit JUnit включва различни компоненти, които са важни за писането на ефективни тестове:

1 Анотации: JUnit предоставя редица анотации, които помагат да се определи поведението на тестовете.

2 Assert Методи: Тези методи са използвани за проверка на условия в кода, които трябва да бъдат изпълнени.

3 Тестови Класове: Тестовите класове съдържат тестови методи, които се изпълняват от JUnit.

4 Тестови Сюити: Тестовите сюити са групи от тестови класове, които се изпълняват заедно.

Анотации в JUnit

  • @Test:

    Анотацията @Test дефинира метод като тестов случай. Това е основният начин за маркиране на метод, който JUnit трябва да изпълни като тест.

    @Test
    public void testAddition() {
    ...
    }
  • @BeforeEach / @Before:

    Изпълнява се преди всяко изпълнение на тестов метод в даден тестов клас.

    @BeforeEach
    public void setup() {
    System.out.println("Подготовка преди всеки тест");
    }
  • @AfterEach / @After:

    Изпълнява се след всеки тестов метод. Обикновено се използва за почистване на ресурси.

    @AfterEach
    public void tearDown() {
    System.out.println("Почистване след всеки тест");
    }
  • @BeforeClass / @BeforeAll:

    Изпълнява код преди стартирането на всички тестове в класа.

    @BeforeClass
    public static void setup() {
    System.out.println("Подготовка преди всички тестове");
    }
  • @AfterClass / @AfterAll:

    Изпълнява код след приключване на всички тестове в класа.

    @AfterClass
    public static void tearDown() {
    System.out.println("Почистване след всички тестове");
    }
  • @DisplayName:

    Анотацията @DisplayName се използва за задаване на четимо име на тестовия метод. Това име се използва в резултатите от изпълнението на теста.

    @Test
    @DisplayName("Тест за събиране")
    public void testAddition() {
    ...
    }
  • @Disabled:

    Анотацията @Disabled се използва за временно изключване на тестов метод. Този метод няма да се изпълни при следващото изпълнение на тестовия клас.

    @Test
    @Disabled("Този тест е временно изключен")
    public void testAddition() {
    ...
    }
  • @Timeout:

    Анотацията @Timeout се използва за задаване на време за изпълнение на тестовия метод. Ако тестът не приключи в рамките на зададеното време, той ще бъде прекъснат.

    @Test
    @Timeout(5)
    public void testAddition() {
    ...
    }
  • @Nested:

    Анотацията @Nested се използва за дефиниране на вложени тестови класове. Това е полезно, когато има няколко тестови класа, които са тясно свързани помежду си.

    @Nested
    class AdditionTests {
    @Test
    public void testAddition() {
    ...
    }
    }
  • @Tag:

    Анотацията @Tag се използва за маркиране на тестови методи с етикети. Това е полезно, когато искаме да изпълним само определени тестове.

    @Test
    @Tag("fast")
    public void testAddition() {
    ...
    }
  • @RepeatedTest:

    Анотацията @RepeatedTest се използва за повтаряне на тестов метод няколко пъти. Това е полезно, когато искаме да изпълним тестове с различни входни данни.

    @RepeatedTest(5)
    public void testAddition() {
    ...
    }
  • @ParameterizedTest:

    Анотацията @ParameterizedTest се използва за изпълнение на тестов метод с различни входни данни. Това е полезно, когато искаме да изпълним тестове с различни входни данни.

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    public void testAddition(int number) {
    ...
    }
  • @CsvSource:

    Анотацията @CsvSource се използва за изпълнение на тестов метод с входни данни от CSV файл. Това е полезно, когато искаме да изпълним тестове с различни входни данни.

    @ParameterizedTest
    @CsvSource({"1, 2, 3", "4, 5, 9"})
    public void testAddition(int a, int b, int expected) {
    ...
    }
  • @CsvFileSource:

    Анотацията @CsvFileSource се използва за изпълнение на тестов метод с входни данни от CSV файл. Това е полезно, когато искаме да изпълним тестове с различни входни данни.

    @ParameterizedTest
    @CsvFileSource(resources = "/data.csv")
    public void testAddition(int a, int b, int expected) {
    ...
    }
  • @MethodSource:

    Анотацията @MethodSource се използва за изпълнение на тестов метод с входни данни от метод. Това е полезно, когато искаме да изпълним тестове с различни входни данни.

    @ParameterizedTest
    @MethodSource("dataProvider")
    public void testAddition(int a, int b, int expected) {
    ...
    }

    static Stream<Arguments> dataProvider() {
    return Stream.of(
    Arguments.of(1, 2, 3),
    Arguments.of(4, 5, 9)
    );
    }
  • @ExtendWith:

    Анотацията @ExtendWith се използва за добавяне на разширения към JUnit. Това е полезно, когато искаме да добавим допълнителна функционалност към JUnit.

    @ExtendWith(MyExtension.class)
    class MyTest {
    ...
    }
  • @RegisterExtension:

    Анотацията @RegisterExtension се използва за регистриране на разширения към JUnit. Това е полезно, когато искаме да добавим допълнителна функционалност към JUnit.

    @RegisterExtension
    static MyExtension myExtension = new MyExtension();

    @Test
    public void testAddition() {
    ...
    }
  • @TempDir:

    Анотацията @TempDir се използва за създаване на временна директория за изпълнение на тестов метод. Това е полезно, когато искаме да изпълним тестове, които изискват временни файлове.

    @Test
    public void testAddition(@TempDir Path tempDir) {
    ...
    }
  • @TempFile:

    Анотацията @TempFile се използва за създаване на временен файл за изпълнение на тестов метод. Това е полезно, когато искаме да изпълним тестове, които изискват временни файлове.

    @Test
    public void testAddition(@TempFile Path tempFile) {
    ...
    }
  • @DisplayNameGeneration:

    Анотацията @DisplayNameGeneration се използва за задаване на генератор на имена на тестови методи. Това е полезно, когато искаме да изпълним тестове с четими имена.

    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class MyTest {
    ...
    }
  • @DisplayNameGenerator:

    Анотацията @DisplayNameGenerator се използва за задаване на генератор на имена на тестови методи. Това е полезно, когато искаме да изпълним тестове с четими имена.

    @DisplayNameGenerator(DisplayNameGenerator.ReplaceUnderscores.class)
    class MyTest {
    ...
    }

Assert Методи

  • assertEquals(expected, actual):

    Проверява дали две стойности са равни.

    assertEquals(5, calculator.add(2, 3));
  • assertNotEquals(expected, actual):

    Проверява дали две стойности не са равни.

    assertNotEquals(5, calculator.add(2, 3));
  • assertSame(expected, actual):

    Проверява дали две стойности са една и съща.

    assertSame(5, calculator.add(2, 3));
  • assertNotSame(expected, actual):

    Проверява дали две стойности не са една и съща.

    assertNotSame(5, calculator.add(2, 3));
  • assertArrayEquals(expected, actual):

    Проверява дали два масива са еднакви.

    assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});
  • assertIterableEquals(expected, actual):

    Проверява дали два итератора са еднакви.

    assertIterableEquals(Arrays.asList(1, 2, 3), Arrays.asList(1, 2, 3));
  • assertLinesMatch(expected, actual):

    Проверява дали два списъка съдържат едни и същи елементи.

    assertLinesMatch(Arrays.asList("1", "2", "3"), Arrays.asList("1", "2", "3"));
  • assertAll(executables):

    Проверява дали всички изпълними кодове не хвърлят изключение.

    assertAll(
    () -> assertEquals(5, calculator.add(2, 3)),
    () -> assertEquals(1, calculator.subtract(3, 2))
    );
  • assertThrows(exception, executable):

    Проверява дали изпълнимият код хвърля изключение.

    assertThrows(IllegalArgumentException.class, () -> calculator.add(null, 3));
  • assertTimeout(timeout, executable):

    Проверява дали изпълнимият код приключва в рамките на определено време.

    assertTimeout(Duration.ofMillis(100), () -> calculator.add(2, 3));
  • fail(message):

    Приключва теста с грешка.

    fail("Тестът е провален");

Примерен Тестов Клас

Този тестов клас демонстрира всички анотации и assert методи в JUnit.

Пример
import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.Arrays;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Примерен Тестов Клас")
class SampleTest {

private Calculator calculator;

@BeforeAll
public static void setup() {
System.out.println("Подготовка преди всички тестове");
}

@BeforeEach
public void init() {
System.out.println("Подготовка преди всеки тест");
calculator = new Calculator();
}

@Test
@DisplayName("Тест за събиране")
public void testAddition() {
assertEquals(5, calculator.add(2, 3));
}

@Test
@DisplayName("Тест за деление с нула")
public void testDivisionByZero() {
assertThrows(IllegalArgumentException.class, () -> calculator.divide(6, 0));
}

@Test
@DisplayName("Тест за събиране с време за изпълнение")
public void testAdditionWithTimeout() {
assertTimeout(Duration.ofMillis(100), () -> calculator.add(2, 3));
}

@Test
@DisplayName("Тест за събиране с всички проверки")
public void testAdditionWithAllAssertions() {
assertAll(
() -> assertEquals(5, calculator.add(2, 3)),
() -> assertEquals(1, calculator.subtract(3, 2))
);
}

@Test
@DisplayName("Тест за събиране с грешка")
public void testAdditionWithFail() {
fail("Тестът е провален");
}

@Nested
class AdditionTests {
@Test
public void testAddition() {
assertEquals(5, calculator.add(2, 3));
}
}

@RepeatedTest(5)
public void testAdditionRepeated() {
assertEquals(5, calculator.add(2, 3));
}

@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
public void testAddition(int number) {
assertEquals(5, calculator.add(2, 3));
}

@ParameterizedTest
@CsvSource({"1, 2, 3", "4, 5, 9"})
public void testAddition(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}

@ParameterizedTest
@CsvFileSource(resources = "/data.csv")
public void testAddition(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}

@ParameterizedTest
@MethodSource("dataProvider")
public void testAddition(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}

static Stream<Arguments> dataProvider() {
return Stream.of(
Arguments.of(1, 2, 3),
Arguments.of(4, 5, 9)
);
}

@Test
@Disabled("Този тест е временно изключен")
public void testAdditionDisabled() {
assertEquals(5, calculator.add(2, 3));
}

@Test
@Timeout(5)
public void testAdditionWithTimeout() {
assertEquals(5, calculator.add(2, 3));
}

@AfterEach
public void tearDown() {
System.out.println("Почистване след всеки тест");
}

@AfterAll
public static void done() {
System.out.println("Почистване след всички тестове");
}
}

Примерен Тестов Сюит

Този тестов сюит демонстрира как да се изпълнят тестови класове с JUnit.

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SuiteDisplayName("Примерен Тестов Сюит")
@SelectPackages("com.example")
@SelectClasses({SampleTest.class, AnotherTest.class})
class SampleTestSuite {
}

Напреднали Техники в JUnit

  • Mocking:

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

    Mocking с Mockito

    Mockito е популярна библиотека за създаване на mock обекти в Java. Тя позволява симулирането на поведение и взаимодействие с обекти без необходимостта от реалната им имплементация.

    @Test
    public void testAddition() {
    Calculator calculator = mock(Calculator.class);
    when(calculator.add(2, 3)).thenReturn(5);
    assertEquals(5, calculator.add(2, 3));
    }

Заключение

Unit тестовете са жизненоважни за създаването на надежден и поддържаем софтуер. JUnit предлага мощен набор от инструменти за писане и изпълнение на тестове в Java, което улеснява контрола на качеството на кода. Правилното използване на JUnit и свързаните с него практики води до по-чист, по-надежден и по-добре структуриран код.