Статьи

Не хватает времени?

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

Но я получаю совершенно другую историю, когда начинаю работать с кодом и спрашиваю: «Где юнит-тесты?» Недавно я спросил своих друзей-программистов онлайн, почему они этого не делают, и, как следствие, почему другие программисты не пишут юнит-тесты. Ответ номер один был тот, который я часто слышу, когда задаю программистам или ИТ-менеджерам один и тот же вопрос: «У меня нет времени» или что-то в этом роде. За этим часто следует аргумент, что для написания приложения с модульными тестами требуется на 20% больше времени, чем без, и «у нас ограничены по времени».

Другие ответы, которые я получил от моего импровизированного онлайн-опроса, который интересовал меня, были «инерция», «не зная как» и «тесты — это не весело». Программирование это весело. Мы бы лучше запрограммировали. (Для полного раскрытия ответ «не зная как» был на вопрос «Почему другие программисты не пишут юнит-тесты?»)

Инерцию я понимаю. Если вы новичок в магазине и у вас есть 4 модульных теста на 2000 объектов, вы, вероятно, не будете противостоять тренду и будете единственным, кто пишет юнит-тесты. Не зная, как это то, что каждый программист должен будет преодолеть.

Мое предложение — поскольку мы пытаемся помочь решить проблему нехватки времени, может быть, мы можем что-то сделать с дефицитом веселья.

На практике

Я работаю над прототипом приложения, которое позволит пользователям вводить информацию о проекте по благоустройству дома, а затем делиться информацией о материалах и инструментах для этого проекта со своими друзьями. Затем друзья могут пообещать одолжить или купить какой-то материал или инструмент, необходимый для проекта. В основном это «Свадебный реестр» для проектов по благоустройству дома.

Тест будет проводиться по методу, который будет принимать объект Project, выполнять итерацию по списку инструментов для этого проекта, чтобы увидеть, был ли этот инструмент уже обещан, и создать список инструментов, которые не были обещаны. Затем он передаст этот список службе, которая будет искать текущую цену каждого инструмента.

Прототип сделан с помощью Grails, но мы напишем этот метод на Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public List<Tool> neededToolList(Project project) {
        final List<Tool> retList = new ArrayList<>();
 
        if (project.getTools() == null || project.getTools().isEmpty()) {
            return retList;
        }
 
        for (Tool tool : project.getTools()) {
            if (!tool.getPromise().isPromised()) {
                retList.add(tool);
            }
        }
 
        List<Tool> tools = lookupService.updateToolList(retList);
        return tools;
    }

Один модульный тест может выглядеть примерно так:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Test
    public void testNeededToolList() {
        Tools _instance = new Tools();
 
        Project project = new Project();
 
        Promise promise = new Promise();
        promise.setProject(project);
        promise.setPromised(false);
        Promise promise2 = new Promise();
        promise2.setProject(project);
        promise2.setPromised(true);
 
        List<Tool> tools = new ArrayList<>();
        List<Tool> lookupTools = new ArrayList<>();
        Tool tool1 = new Tool();
        tool1.setName("table saw");
        tool1.setStoreId("T001");
        tool1.setPromise(promise);
        tools.add(tool1);
        lookupTools.add(tool1);
        Tool tool2 = new Tool();
        tool2.setName("pneumatic nail guns");
        tool2.setStoreId("T027");
        tool2.setPromise(promise2);
        tools.add(tool2);
        project.setTools(tools);
 
        List<Tool> mockedTools = new ArrayList<>();
        Tool mockedTool1 = new Tool();
        mockedTool1.setPromise(promise);
        mockedTool1.setName("table saw");
        mockedTool1.setStoreId("T001");
        mockedTool1.setPrice(129.0);
        mockedTools.add(mockedTool1);
 
        lookupService = Mockito.mock(LookupServiceImpl.class);
        Mockito.when(lookupService.updateToolList(lookupTools)).thenReturn(mockedTools);
        _instance.setLookupService(lookupService);
 
        List<Tool> returnedTools  = _instance.neededToolList(project);
 
        assertTrue(returnedTools.size() == 1);
        for(Tool tool : returnedTools) {
            assertEquals(129.0, tool.getPrice(), 0.01);
        }
    }

Это простой и только один тест. Должны быть написаны тесты для нескольких ситуаций, таких как нулевые значения. Что, если StoreID не существует, например?

Введите Groovy

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

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

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
class GroovyToolsTest extends GroovyTestCase {
    def lookupService = [
        updateToolList : {List<Tool> toolList ->
            toolList.each {
                if(it.storeId == "T001") {
                    it.price = 129.0
                }
            }
            return toolList
        }
    ] as LookupService
 
    void testNeededToolList() {
        def _instance = new Tools()
        def project = new Project()
        project.tools = [
            new Tool(name: "table saw", storeId: "T001", promise: new Promise(project: project, promised: false)),
            new Tool(name: "pneumatic nail guns", storeId: "T027", promise: new Promise(project: project, promised: true))
        ]
 
        _instance.lookupService = lookupService
 
        def returnedList = _instance.neededToolList(project)
        returnedList.size() == 1
        returnedList.each {
            if(it.storeId == "T001") {
                assert it.price == 129.0
            }
        }
        println "done"
    }
}

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

Помните: я не тестирую службу, поэтому служба Mocked должна возвращать значение, которое, как я ожидаю, вернет служба.

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

Если вы хотите думать немного по-другому для своих модульных тестов, существует также среда тестирования Spock. У него более обширный язык, который придает ему более ориентированный на поведение внешний вид, но он по-прежнему использует все Groovy Goodness из предыдущего примера.

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
33
34
35
class ToolsSpec extends Specification {
    def lookupService = [
        updateToolList : {List<Tool> toolList ->
            println "mocked service"
            toolList.each { tool ->
                if(tool.storeId == "T001")
                    tool.price = 129.0
            }
            return toolList
        }
    ] as LookupService
 
    def "Lookup needed tool list"() {
        given:"Create instance"
            def _instance = new Tools()
            def project = new Project()
            project.tools = [
                [name: "table saw", storeId: "T001", promise: [project: project, promised: false] as Promise] as Tool,
                [name: "pneumatic nail guns", storeId: "T027", promise: [project: project, promised: true] as Promise] as Tool,
                ] as List<Tool>;
 
        _instance.lookupService = lookupService
 
        expect:"Tool List"
            def returnedList = _instance.neededToolList(project)
            returnedList.size() == 1
            returnedList.each {
                if(it.storeId == "T001") {
                    assert it.price == 129.0
                }
            }
 
    }
 
}

Обратите внимание, что я использовал другой синтаксис для создания объектов тестовых данных для Tool. Это стандартная функция Groovy, которая позволяет программисту конвертировать карту в конкретный класс и могла также использоваться в предыдущем примере. Когда вы привыкнете читать Groovy, это может быть проще для чтения, чем новый синтаксис Object.

В обоих примерах более жесткий код с синтаксисом «сахар» — не единственное преимущество. Результат неудачного теста также отличается и гораздо более полезен

Результат неудачного теста в первом примере:

1
2
3
4
5
6
7
java.lang.AssertionError: expected:<128.0> but was:<129.0>
    at org.junit.Assert.fail(Assert.java:88)
    at org.junit.Assert.failNotEquals(Assert.java:834)
    at org.junit.Assert.assertEquals(Assert.java:553)
    at org.junit.Assert.assertEquals(Assert.java:683)
    at org.projectregistry.services.ToolsTest.testNeededToolList(ToolsTest.java:93)
....

Результаты тестов Groovy и Spock выглядят так:

01
02
03
04
05
06
07
08
09
10
11
Assertion failed:
 
assert it.price == 128.0
       |  |     |
       129.0 false
       org.projectregistry.model.Tool@5e59238b
 
    at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:399)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:648)
    at org.projectregistry.services.GroovyToolsTest$_testNeededToolList_closure2.doCall(GroovyToolsTest.groovy:34)
...

В выводе Groovy содержится намного больше информации, что, в свою очередь, дает возможность быстрее исправить.

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

Учиться как просто. И Groovy, и Spock хорошо документированы, и есть много ресурсов, которые можно найти только в Google *. Существует также очень живое и полезное сообщество в различных социальных сетях, которое, я уверен, с удовольствием поможет.