Статьи

Автоматизированные тесты для асинхронных процессов

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

СОВЕТ: Если вы обнаружите необходимость асинхронного тестирования в своих модульных тестах, вы, вероятно, делаете что-то не так, и вам нужно перепроектировать код, чтобы отделить эти проблемы.

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

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

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

  1. Убрать асинхронное поведение
  2. Опрос, пока у вас есть желаемое состояние

Убрать асинхронное поведение

Я использовал этот подход, когда много лет назад TDD-приложение для толстых клиентов использовалось, когда написание приложений в Swing-приложениях было все еще распространенным подходом. Для этого необходимо изолировать действие, вызывающее поведение, в одном месте, которое вместо того, чтобы происходить в другом потоке, в процессе тестирования происходило бы в том же потоке, что и тест. Я даже выступил с докладом об этом в 2006 году и написал эту таблицу, рассказывающую о процессе.

Этот подход требовал дисциплинированного подхода к проектированию, когда переключение этого поведения было изолировано в одном месте.

Опрос, пока у вас есть желаемое состояние

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

Тайм-ауты — это еще одно проклятие асинхронного поведения, потому что вы на самом деле не знаете, когда произойдет какое-либо действие, но вы не хотите, чтобы тест проходил вечно.

В прошлый раз, когда мне приходилось что-то делать, мы часто заканчивали тем, что писали свои собственные опросы и перехват времени ожидания, в то время как относительно простая теперь доступна как очень простая библиотека. К счастью, другие люди также столкнулись с этой проблемой в java-land и предоставили библиотеку, чтобы упростить тестирование в форме Awaitility .

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

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.thekua.spikes.aysnc.testing;
 
import com.thekua.spikes.aysnc.testing.FileGenerator;
import org.junit.Before;
import org.junit.Test;
 
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
 
public class FileGeneratorTest {
 
    private static final String RESULT_FILE = "target/test/resultFile.txt";
    private static final String STEP_1_LOG = "target/test/step1.log";
    private static final String STEP_2_LOG = "target/test/step2.log";
    private static final String STEP_3_LOG = "target/test/step3.log";
 
    private static final List<String> FILES_TO_CLEAN_UP = Arrays.asList(STEP_1_LOG, STEP_2_LOG, STEP_3_LOG, RESULT_FILE);
 
 
    @Before
    public void setUp() {
        for (String fileToCleanUp : FILES_TO_CLEAN_UP) {
            File file = new File(fileToCleanUp);
            if (file.exists()) {
                file.delete();
            }
        }
    }
 
 
    @Test
    public void shouldWaitForAFileToBeCreated() throws Exception {
        // Given I have an aysnc process to run
        String expectedFile = RESULT_FILE;
 
        List<FileGenerator> fileGenerators = Arrays.asList(
                new FileGenerator(STEP_1_LOG, 1, "Step 1 is complete"),
                new FileGenerator(STEP_2_LOG, 3, "Step 2 is complete"),
                new FileGenerator(STEP_3_LOG, 4, "Step 3 is complete"),
                new FileGenerator(expectedFile, 7, "Process is now complete")
        );
 
        // when it is busy doing its work
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (final FileGenerator fileGenerator : fileGenerators) {
            executorService.execute(new Runnable() {
                public void run() {
                    fileGenerator.generate();
                }
            });
        }
 
        // then I get some log outputs
        await().atMost(2, SECONDS).until(testFileFound(STEP_1_LOG));
        await().until(testFileFound(STEP_2_LOG));
        await().until(testFileFound(STEP_3_LOG));
 
        // and I should have my final result with the output I expect
        await().atMost(10, SECONDS).until(testFileFound(expectedFile));
        String fileContents = readFile(expectedFile);
        assertThat(fileContents, startsWith("Process"));
 
        // Cleanup
        executorService.shutdown();
    }
 
    private String readFile(String expectedFile) throws IOException {
        return new String(Files.readAllBytes(Paths.get(expectedFile)));
 
    }
 
 
    private Callable<Boolean> testFileFound(final String file) {
        return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                return new File(file).exists();
            }
        };
    }
}

Вы можете изучить полный демонстрационный код в этом общедоступном репозитории git .