Статьи

Модульное тестирование с помощью JUnit — часть 2

В первой части серии, посвященной модульному тестированию с использованием JUnit, мы рассмотрели создание модульных тестов с использованием Maven и IntelliJ. В этой статье мы рассмотрим некоторые концепции базового модульного тестирования и применим те, которые используют конструкции JUnit. Мы узнаем об утверждениях, аннотациях JUnit 4 и тестовых наборах.

JUnit Утверждения

Утверждения, или просто утверждения, предоставляют программистам способ проверки предполагаемого поведения кода. Например, с помощью утверждения вы можете проверить, возвращает ли метод ожидаемое значение для данного набора параметров, или метод правильно устанавливает некоторые переменные экземпляра или класса. Когда вы запускаете тест, утверждение выполняется. Если тестируемый метод ведет себя точно так, как вы указали в утверждении, ваш тест проходит. В противном случае выдается ошибка AssertionError .

JUnit обеспечивает поддержку утверждений с помощью набора методов assert в классе org.junit.Assert . Прежде чем мы начнем их использовать, давайте кратко рассмотрим шаблон Arrange, Act, Assert (AAA). Этот шаблон является рекомендуемым способом написания методов модульного тестирования, где вы делите метод на три раздела, каждый со своей определенной целью:

  • Arrange : инициализируйте объекты и настройте входные данные для тестируемого метода.
  • Действие : вызвать тестируемый метод, передав упорядоченные параметры.
  • Утверждение : убедитесь, что тестируемый метод работает должным образом. Здесь вы пишете метод утверждения.

Вот класс Java, мы будем писать некоторые тесты JUnit для тестирования.

EmployeeEmail.java

package guru.springframework.unittest.asserts;

import java.util.HashMap;
import java.util.Map;
import  java.util.regex.*;

public class EmployeeEmail {

    Map<String, String> hashMap = new HashMap<String, String>();

    public  void addEmployeeEmailId(String key, String value){
        if(isValidEmailId(value)) {
            hashMap.put(key, value);
        }
    }
    public String getEmployeeEmailId(Object key){
        if (!(key instanceof String)) {
            throw new IllegalArgumentException("Object not type of String");
        }
        return hashMap.get(key);
    }
    public boolean isValidEmailId(String email){
        String regex = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$";
        Pattern pattern = Pattern.compile(regex);
        Matcher m = pattern.matcher(email);
        return m.matches();
    }
}

В EmployeeEmailвышеприведенном классе мы написали addEmployeeEmailId()метод, который сначала проверяет, имеет ли идентификатор электронной почты действительный формат, а затем добавляет его в Mapреализацию. isValidEmailId()Метод выполняет проверку электронной почты , используя регулярное выражение . Мы также написали getEmployeeEmailId()метод для возврата идентификатора электронной почты из Mapданного ключа.

Чтобы протестировать EmployeeEmailкласс, мы создадим тестовый класс EmployeeEmailTestи добавим в него методы тестирования. Здесь следует помнить, что количество добавляемых тестовых методов и то, что они должны делать, зависит от поведения EmployeeEmailтестируемого класса, а не от количества методов в нем.

Для начала мы проверим, что getEmployeeEmailId()метод возвращает trueдействительный идентификатор электронной почты и falseнедействительный с двумя методами тестирования.

. . .
@Test
public void testValidEmailId() throws Exception {
    /*Arrange*/
    EmployeeEmail empEmail=new EmployeeEmail();
    /*Act*/
    boolean result = empEmail.isValidEmailId("andy@testdomain.com");
    /*Assert*/
    assertTrue("Valid email ID failed ", result );
}

@Test
public void testInvalidEmailId() throws Exception {
    /*Arrange*/
    EmployeeEmail empEmail=new EmployeeEmail();
    /*Act*/
    boolean result= empEmail.isValidEmailId("andy@testdomain");
    /*Assert*/
    assertFalse("Invalid email ID passed ", result);
}
. . .

В обоих вышеописанных методах мы разделили тестовый код на разделы AAA. В первом тестовом методе мы использовали assertTrue()метод, поскольку ожидаем isValidEmailId()возврата trueдля идентификатора электронной почты andy@testdomain.com. Мы также хотим проверить, что isValidEmailId()возвращает falseнеправильный идентификатор электронной почты. Для этого мы написали второй тестовый метод и использовали его assertFalse().

Несколько вещей, чтобы наблюдать здесь. В обоих методах подтверждения мы передали параметр String в качестве идентифицирующего сообщения для ошибки подтверждения. Программисты обычно устанавливают это сообщение, чтобы описать условие, которое должно быть выполнено. Вместо этого, чтобы иметь смысл, это сообщение должно описывать, что не так, если условие не выполняется.

Кроме того, вы можете подумать: « Почему два отдельных метода тестирования вместо одного метода с обоими методами assert? «Наличие нескольких методов подтверждения в одном методе тестирования не приведет к ошибкам в тестах, и вы часто будете сталкиваться с такими методами тестирования. Но следует придерживаться хорошего правила: « Правильные юнит-тесты должны проваливаться ровно по одной причине », что звучит аналогично принципу единой ответственности, В методе проверки с ошибками, имеющем несколько утверждений, требуется больше усилий, чтобы определить, какое утверждение не удалось. Кроме того, не гарантируется, что все утверждения имели место. Для непроверенного исключения утверждения после исключения не будут выполнены, и JUnit переходит к следующему методу тестирования. Поэтому, как правило, рекомендуется использовать одно утверждение для каждого метода тестирования.

Имея основы, давайте напишем полный тестовый класс и используем следующие утверждения:

  • assertEquals () и assertNotEquals () : проверяет, равны ли два примитива / объекта. В дополнение к строковому сообщению, передаваемому в качестве первого параметра, эти методы принимают ожидаемое значение в качестве второго параметра и фактическое значение в качестве третьего параметра — важный порядок, который обычно используется неправильно.
  • assertNull () и assertNotNull () : проверяет, является ли объект нулевым или нет.
  • assertSame ()  и assertNotSame () : проверяет, указывают ли две ссылки на один и тот же объект.

EmployeeEmailTest.java

package guru.springframework.unittest.asserts;
import org.junit.Test;

import java.util.Map;

import static org.junit.Assert.*;

public class EmployeeEmailTest {
    @Test
    public void testValidEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        /*Act*/
        boolean result = empEmail.isValidEmailId("andy@testdomain.com");
        /*Assert*/
        assertTrue("Valid email ID failed ", result );
    }

    @Test
    public void testInvalidEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        /*Act*/
        boolean result= empEmail.isValidEmailId("andy@testdomain");
        /*Assert*/
        assertFalse("Invalid email ID passed ", result);
    }

    @Test
    public void testAddEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","peter@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "mary@testdomain.com");
        /*Act*/
        int size=empEmail.hashMap.size();
        /*Assert*/
        assertEquals("Incorrect collection size ", 2, size);
    }
    @Test
    public void testAddEmailIdWithDuplicateKey() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","peter@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "mary@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "henry@testdomain.com");
        /*Act*/
        int size=empEmail.hashMap.size();
        /*Assert*/
        assertNotEquals("Duplicate key in collection ", 3, size);
    }

    @Test
    public void testGetExistingEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","peter@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "mary@testdomain.com");
        /*Act*/
        String val = empEmail.getEmployeeEmailId("Emp02");
        /*Assert*/
        assertNotNull("Returned null for existing employee", val);
    }

    @Test
    public void testGetNonExistingEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","peter@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "mary@testdomain.com");
       /*Act*/
        String val = empEmail.getEmployeeEmailId("Emp05");
       /*Assert*/
        assertNull("Failed to return null for non existing employee", val);
    }

    @Test
    public void testIfObjectsAreSame() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail1=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp01","peter@testdomain.com");
        EmployeeEmail empEmail2=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp02", "mary@testdomain.com");
        /*Act*/
        Map map1=empEmail1.hashMap;
        Map map2=empEmail2.hashMap;
        map1= map2;
        /*Assert*/
        assertSame("Failed because objects are not same ", map1, map2);
    }

    @Test
    public void testIfObjectsAreNotSame() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail1=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp01","peter@testdomain.com");
        EmployeeEmail empEmail2=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp02", "mary@testdomain.com");
        /*Act*/
        Map map1=empEmail1.hashMap;
        Map map2=empEmail2.hashMap;
        /*Assert*/
        assertNotSame("Failed because objects are same ", map1, map2);
    }


}

В EmployeeEmailTestклассе выше:

  • Строка 38 : мы использовали assertEquals()для проверки размера коллекции после добавления к ней двух элементов addEmployeeEmailId().
  • Строка 50 : Раньше мы assertNotEquals()проверяли, что коллекция не позволяет добавлять дубликаты ключей addEmployeeEmailId().
  • Строка 62 : мы использовали assertNotNull()для проверки, что getEmployeeEmailId()не возвращает nullидентификатор электронной почты, присутствующий в коллекции.
  • Строка 74 : мы использовали assertNull()для проверки того, что getEmployeeEmailId()возвращается nullидентификатор электронной почты, отсутствующий в коллекции.
  • Строка 89 : мы использовали assertSame()для проверки того, что две ссылки на коллекцию указывают на один и тот же объект коллекции после назначения одной другой через =оператор.
  • Строка 103 : мы использовали assertNotSame()для проверки того, что две ссылки на коллекцию не указывают на один и тот же объект.

Когда мы запускаем тест в IntelliJ, вывод:
Вывод в IntelliJ

Как видно из вывода, все тесты пройдены, как и ожидалось.

Примечание . Порядок, в котором JUnit выполняет методы тестирования, не гарантируется, поэтому не рассчитывайте на это.

Если вы вернетесь назад и загляните в тестовый класс, вы заметите, что несколько строк кода в части Arrange повторяются для всех тестовых методов. В идеале они должны быть в одном месте и выполняться перед каждым тестом. Мы можем достичь этого с помощью аннотаций JUnit, которые мы рассмотрим далее.

Юнит Аннотации

Вы можете использовать аннотации JUnit, представленные в JUnit 4, для маркировки и настройки методов тестирования. Мы уже использовали @Testаннотацию, чтобы пометить открытые методы void как методы тестирования. Когда JUnit встречает метод, аннотированный @Test, он создает новый экземпляр класса, а затем вызывает метод. При желании мы можем предоставить timeoutпараметр @Testдля указания времени, измеряемого в миллисекундах. Если метод тестирования выполняется дольше, чем указанное время, проверка завершается неудачно. Это особенно полезно при тестировании на производительность с точки зрения времени. Этот код помечает метод как метод тестирования и устанавливает время ожидания в 100 миллисекунд.

. . .
@Test(timeout = 100)
public void testDataAccessTimeout(){
    String val = empEmail.getEmployeeEmailId("Emp02");
}
. . .

Другое важное использование @Testаннотации — проверка исключений. Предположим, что для условия код выдает исключение. Мы можем использовать @Testаннотацию, чтобы проверить, действительно ли код генерирует исключение при выполнении условия. Этот код проверяет, getEmployeeEmailId()генерирует ли метод исключение типа, IllegalArgumentExceptionкогда ему передается не строковое значение.

. . .
@Test(expected = IllegalArgumentException.class)
public void testForIllegalArgumentException()
{
    String val = empEmail.getEmployeeEmailId(1);

}
. . .

В дополнение к аннотации @Test , другие аннотации:

  • @Before : заставляет метод запускаться перед каждым тестовым методом класса. Обычно вы используете эту аннотацию для выделения ресурса, установки общего кода инициализации и загрузки файлов конфигурации, необходимых для методов тестирования.
  • @After : Заставляет метод запускаться после каждого тестового метода класса. Этот метод гарантированно запускается, даже если метод @Before или @Test выдает исключение. Используйте эту аннотацию для очистки кода инициализации и освобождения любых ресурсов, выделенных в @Before .
  • @BeforeClass : статический метод запускается один и только один раз перед любым из тестовых методов в классе. Это полезно в ситуациях, когда вам нужно настроить вычислительно дорогие ресурсы, например, соединение с сервером, базу данных или даже управление встроенным сервером для тестирования. Например, вместо запуска сервера для каждого метода @Test , запустите его один раз в методе @BeforeClass для всех тестов в классе.
  • @ AfterClass : статический метод запускается один раз после завершения всех тестовых методов в классе. Этот метод гарантированно запускается, даже если метод @BeforeClass или @Test выдает исключение. Используйте этот метод, чтобы освободить однократную инициализацию ресурса, выполненную в @BeforeClass .
  • @Ignore : JUnit игнорирует метод тестирования. Это может быть полезно, когда у вас есть сложный кусок кода, который находится в процессе перехода, и вы можете временно отключить некоторые тесты, пока этот код не будет готов. Участники тестов в большинстве сред IDE сообщают о том, что @Ignore тесты являются напоминаниями во время каждого запуска теста. По сути, это означает помечать тесты как «что-то, что нужно сделать», что в противном случае вы можете забыть, если закомментируете метод теста или удалите аннотацию @Test .

Вот пример использования всех аннотаций JUnit.

EmployeeEmailAnnotationsTest.java

package guru.springframework.unittest.asserts;
import org.junit.*;
import java.util.Map;
import static org.junit.Assert.*;

public class EmployeeEmailAnnotationsTest {
    EmployeeEmail empEmail;
    static int num;
    @BeforeClass
    public static void oneTimeSetup(){
     num=1;
     System.out.println("JUnit Call:"+num+" @BeforeClass oneTimeSetup");
    }
    @Before
    public void setup(){
        num+=1;
        System.out.println("JUnit Call:"+num+" @Before setUp");
        empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","peter@testdomain.com");
        empEmail.addEmployeeEmailId("Emp02", "mary@testdomain.com");
    }
    @After
    public void cleanup()
    {
        num+=1;
        System.out.println("JUnit Call:" + num + " @After cleanup");
        empEmail.hashMap.clear();
    }
    @AfterClass
    public static void oneTimeCleanup()
    {
        num+=1;
        System.out.println("JUnit Call:"+num+" @AfterClass oneTimeCleanup");
        num=0;
    }
    @Test(timeout = 100)
    public void testDataAccessTimeout(){
        num+=1;
        System.out.println("JUnit Call:"+num+" @Test testDataAccessTimeout");
        String val = empEmail.getEmployeeEmailId("Emp02");
    }
    @Test
    @Ignore("Test code not ready")
    public void testWithMoreData(){
        /*ToDO: */
    }
    @Test(expected = IllegalArgumentException.class)
    public void testForIllegalArgumentException()
    {
        num+=1;
        System.out.println("JUnit Call:" + num + " @Test testForIllegalArgumentException");
        String val = empEmail.getEmployeeEmailId(1);

    }
}

Результат запуска теста в IntelliJ:

JUnit вывод в IntelliJ

JUnit Test Suites

Если у вас есть большое количество классов тестов для разных функциональных областей или модулей, вы можете структурировать их в наборы тестов. Наборы тестов JUnit являются контейнерами тестовых классов и дают вам более точный контроль над порядком выполнения тестовых классов. JUnit предоставляет org.junit.runners.Suiteкласс, который запускает группу тестовых классов.
Код для создания набора тестов:

EmployeeEmailTestSuite.java

package guru.springframework.unittest.testsuite;

import guru.springframework.unittest.asserts.EmployeeEmailAnnotationsTest;
import guru.springframework.unittest.asserts.EmployeeEmailTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
        EmployeeEmailTest.class,
        EmployeeEmailAnnotationsTest.class

})
public class EmployeeEmailTestSuite {
}

В приведенном выше классе тестового набора мы написали две аннотации: @RunWithи @SuiteClasses. @RunWithАннотацию инструктирует JUnit использовать Suiteкласс бегунка и @SuiteClassesопределяет классы и их порядок , что Suiteкласс бегун должен работать. Класс набора тестов сам по себе пуст и действует только как заполнитель для аннотаций.

Вывод при выполнении набора тестов в IntelliJ:

Тестовый набор JUnit Вывод в IntelliJ

Резюме

JUnit Assertions не только делает ваш код стабильным, но и заставляет вас думать по-другому и продумывать различные сценарии, что в конечном итоге помогает вам стать лучшими программистами. Понимая цель различных утверждений и используя их правильно, тестирование становится эффективным. Но вопрос заключается в следующем: « Сколько подтверждений на метод испытаний?». Все сводится к сложности тестируемого метода. Для метода с несколькими условными операторами должно быть сделано утверждение результата для каждого условия, в то время как для метода, выполняющего простую манипуляцию строк, должно быть выполнено одно утверждение. При разработке модульных тестов с помощью JUnit рекомендуется, чтобы каждый метод тестирования проверял определенное условие, что часто приводит к одному утверждению на метод тестирования. Нередко тестируемый метод ассоциируется с несколькими методами тестирования.
Одно утверждение, которое я не рассмотрел в этом посте, это assertThat () . Это важное утверждение JUnit, о котором я расскажу в следующем посте о JUnit.

Модульное тестирование с Spring Framework

Во время разработки корпоративных приложений с помощью Spring Framework и модульного тестирования вашего кода вы будете использовать множество утверждений. В дополнение к утверждению обычного поведения методов, вы будете утверждать, внедрены ли bean-компоненты Spring в соответствии с ожиданиями контекста приложения Spring, правильно ли поддерживаются зависимости между bean-компонентами Spring и т. Д. При создании этих тестов убедитесь, что они выполняются быстро, особенно когда тестирование включено в цикл сборки. Вы продолжите собирать свое приложение по мере написания кода, и, таким образом, вы, очевидно, не захотите, чтобы ваша сборка ожидала завершения длинного теста. Если у вас есть такие длительные тесты, поместите их в отдельный набор тестов.