Статьи

Написание чистых тестов — оно начинается с конфигурации

Довольно сложно найти хорошее определение чистого кода, потому что у каждого из нас есть собственное определение слова «чистый». Однако есть одно определение, которое кажется универсальным:

Чистый код легко читается.

Это может удивить некоторых из вас, но я думаю, что это определение относится и к тестовому коду. В наших интересах сделать наши тесты максимально удобочитаемыми, потому что:

  • Если наши тесты легко читаются, легко понять, как работает наш код.
  • Если наши тесты легко читаются, легко найти проблему, если тест не пройден (без использования отладчика).

Писать чистые тесты несложно, но это требует много практики, и поэтому многие разработчики борются с этим.

Я тоже с этим боролся, и поэтому решил поделиться с вами своими выводами.

Это первая часть моего урока, которая описывает, как мы можем писать чистые тесты. На этот раз мы узнаем, как мы можем настроить наши тестовые примеры простым и понятным способом.

Проблема

Предположим, что мы должны написать «модульные тесты» для контроллеров Spring MVC, используя среду тестирования Spring MVC . Первый контроллер, который мы собираемся протестировать, называется TodoController , но мы должны написать «модульные тесты» и для других контроллеров нашего приложения.

Как разработчики, мы знаем, что дублированный код — это плохо. Когда мы пишем код, мы следуем принципу « Не повторяйся (СУХОЙ)», который гласит:

Каждая часть знаний должна иметь одно, однозначное, авторитетное представление в системе.

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

Давайте посмотрим, как мы можем настроить наши «модульные тесты», используя подход.

Во-первых , нам нужно создать абстрактный базовый класс, который настраивает среду Spring MVC Test и гарантирует, что ее подклассы могут обеспечить дополнительную конфигурацию путем реализации метода setUpTest (MockMvc mockMvc) .

Исходный код класса AbstractControllerTest выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public abstract class AbstractControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private WebApplicationContext webAppContext;
 
    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
        setupTest(MockMvc mockMvc)
    }
    
    protected abstract void setUpTest(MockMvc mockMvc);
}

Во-вторых , мы должны реализовать реальный тестовый класс, который создает необходимые макеты и новый объект контроллера. Исходный код класса TodoControllerTest выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;
 
public class TodoControllerTest extends AbstractControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService serviceMock;
    
    @Override
    protected void setUpTest(MockMvc mockMvc) {
        Mockito.reset(serviceMock);
        this.mockMvc = mockMvc;
    }
 
    //Add test methods here
}

Этот тестовый класс выглядит довольно чисто, но у него есть один существенный недостаток:

Если мы хотим выяснить, как настроены наши тестовые примеры, мы должны прочитать исходный код классов TodoControllerTest и AbstractControllerTest .

Это может показаться незначительной проблемой, но это означает, что мы должны перенести наше внимание с тестовых случаев на базовый класс (или классы). Это требует умственного переключения контекста, а переключение контекста ОЧЕНЬ дорого .

Можно, конечно, утверждать, что ментальная цена использования наследования в этом случае довольно низкая, потому что конфигурация довольно проста. Это правда, но хорошо помнить, что это не всегда так в реальных приложениях.

Реальная стоимость переключения контекста зависит от глубины иерархии тестовых классов и сложности нашей конфигурации.

Решение

Мы можем улучшить удобочитаемость нашей конфигурации, настроив все тестовые примеры в классе тестирования. Это означает, что мы должны:

  • Добавьте необходимые аннотации (например, @RunWith ) в тестовый класс.
  • Добавьте методы setup и teardown в тестовый класс.

Если мы изменим наш примерный тестовый класс, следуя этим правилам, его исходный код будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
    
    @Autowired
    private TodoService serviceMock;
 
    @Autowired
    private WebApplicationContext webAppContext;
 
    @Before
    public void setUp() {
        Mockito.reset(serviceMock);
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
    }
 
    //Add test methods here
}

На мой взгляд, новая конфигурация наших тестовых примеров выглядит намного проще и чище, чем старая конфигурация, которая была разделена на классы TodoControllerTest и AbstractControllerTest .

К сожалению, ничего не бесплатно.

Это компромисс

Каждое решение по разработке программного обеспечения — это компромисс, в котором есть как плюсы, так и минусы. Это не исключение из этого правила .

Конфигурирование наших тестовых примеров в классе тестирования имеет следующие преимущества:

  1. Мы можем понять конфигурацию наших тестовых случаев, не читая все суперклассы тестового класса. Это экономит много времени, потому что нам не нужно переключать наше внимание с одного класса на другой. Другими словами, нам не нужно платить за переключение контекста .
  2. Это экономит время, когда тест не пройден. Если бы мы использовали наследование, потому что мы хотим избежать дублирования кода или конфигурации, есть вероятность, что наши базовые классы будут содержать компоненты, которые имеют отношение к некоторым, но не ко всем тестовым примерам. Другими словами, мы должны были бы выяснить, какие компоненты имеют отношение к неудачному тестовому примеру, и это может быть не простой задачей. Когда мы настраиваем наши тестовые примеры в классе тестирования, мы знаем, что каждый компонент имеет отношение к неудачному тестовому сценарию .

С другой стороны, минусы этого подхода:

  1. Мы должны написать повторяющийся код. Это занимает больше времени, чем установка необходимой конфигурации для базового класса (или классов).
  2. Если какая-либо из используемых библиотек изменяется таким образом, что заставляет нас изменять конфигурацию наших тестов, мы должны внести необходимые изменения в каждый тестовый класс. Это, очевидно, намного медленнее, чем делать их только для базового класса (или классов).

Если наша единственная цель состоит в том, чтобы писать наши тесты как можно быстрее, ясно, что мы должны устранить дублирующийся код и конфигурацию.

Однако это не единственная моя цель.

Есть две причины, почему я считаю, что преимущества этого подхода перевешивают его недостатки:

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

Моя позиция в этом вопросе кристально чиста. Однако остается еще один очень важный вопрос:

Вы сделаете другой компромисс?