Статьи

Добавление ведения базы данных в JUnit3

За последнее десятилетие мы написали тысячи тестов JUnit3 и сейчас пытаемся объединить результаты в базе данных вместо разбросанных лог-файлов. Оказывается, для этого невероятно легко расширить класс TestCase. Примечание: этот подход не применяется напрямую к JUnit4 или другим тестовым фреймворкам, но обычно возможно сделать что-то аналогичное.

Тестируемый класс и его тест

В демонстрационных целях мы можем определить класс с единственным методом для тестирования.

1
2
3
4
5
6
public class MyTestedClass {
 
    public String op(String a, String b) {
        return ((a == null) ? "" : a) + ":" + ((b == null) ? "" : b);
    }
}

Класс с одним тестируемым методом — меньшее ограничение, чем вы думаете. Мы тестируем только четыре метода в тысячах тестов, которые я упоминал ранее.

Вот несколько тестов для класса выше.

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
public class MySimpleTest extends SimpleTestCase {
 
    private MyTestedClass obj = new MyTestedClass();
 
    public void test1() {
        assertEquals("a:b", obj.op("a", "b"));
    }
 
    public void test2() {
        assertEquals(":b", obj.op(null, "b"));
    }
 
    public void test3() {
        assertEquals("a:", obj.op("a", null));
    }
 
    public void test4() {
        assertEquals(":", obj.op(null, null));
    }
 
    public void test5() {
        // this will fail
        assertEquals(" : ", obj.op(null, null));
    }
}

Сбор базовой информации с помощью TestListener

JUnit3 позволяет слушателям добавлять свои тестовые процессы. Этот слушатель вызывается до и после запуска теста, а также каждый раз, когда тест завершается неудачно или имеет ошибку (выдает исключение). Этот TestListener записывает базовую тестовую информацию в System.out в качестве доказательства концепции. Было бы легко изменить его, чтобы записать информацию в базу данных, тему JMS и т. Д.

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
public class SimpleTestListener implements TestListener {
    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
    private long start;
    private boolean successful = true;
    private String name;
    private String failure = null;
 
    SimpleTestListener() {
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public void startTest(Test test) {
        start = System.currentTimeMillis();
    }
 
    public void addError(Test test, Throwable t) {
        // cache information about error.
        successful = false;
    }
 
    public void addFailure(Test test, AssertionFailedError e) {
        // cache information about failure.
        failure = e.getMessage();
        successful = false;
    }
 
    /**
     * After the test finishes we can update the database with statistics about
     * the test - name, elapsed time, whether it was successful, etc.
     */
    public void endTest(Test test) {
        long elapsed = System.currentTimeMillis() - start;
 
        SimpleDateFormat fmt = new SimpleDateFormat();
        fmt.setTimeZone(UTC);
 
        System.out.printf("[%s, %s, %s, %d, %s, %s]\n", test.getClass().getName(), name, fmt.format(new Date(start)),
                elapsed, failure, Boolean.toString(successful));
 
        // write any information about errors or failures to database.
    }
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public class SimpleTestListenerFactory {
    public static final SimpleTestListenerFactory INSTANCE = new SimpleTestListenerFactory();
 
    public SimpleTestListenerFactory() {
        // establish connection data source here?
    }
 
    public SimpleTestListener newInstance() {
        // initialize listener.
        SimpleTestListener listener = new SimpleTestListener();
        return listener;
    }
}

Если мы знаем, что тестовый фреймворк является чисто последовательным, мы можем перехватить весь вывод консоли, создав буфер и вызвав System.setOut () в startTest (), а затем восстановив исходный System.out в endTest (). Это работает хорошо до тех пор, пока тесты никогда не пересекаются, но в противном случае могут возникнуть проблемы Это может быть проблематично — в IDE могут быть свои собственные тестовые прогоны, которые допускают параллельное выполнение.

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

01
02
03
04
05
06
07
08
09
10
public class SimpleTestCase extends TestCase {
  
    public void run(TestResult result) {
        SimpleTestListener l = SimpleTestListenerFactory.INSTANCE.newInstance();
        result.addListener(l);
        l.setName(getName());
        super.run(result);
        result.removeListener(l);
    }
}

Теперь мы получаем ожидаемые результаты в System.out.

1
2
3
4
5
[MySimpleTest, test1, 8/2/15 11:58 PM, 0, null, true]
[MySimpleTest, test2, 8/2/15 11:58 PM, 10, null, true]
[MySimpleTest, test3, 8/2/15 11:58 PM, 0, null, true]
[MySimpleTest, test4, 8/2/15 11:58 PM, 0, null, true]
[MySimpleTest, test5, 8/2/15 11:58 PM, 4, expected same:<:> was not:< : >, false]

Сбор информации о вызовах с помощью фасада и TestListener

Это хорошее начало, но мы могли бы добиться большего. В тысячах тестов, упомянутых выше, вызывается только 4 метода — было бы чрезвычайно эффективно, если бы мы могли фиксировать входные и выходные значения этих вызовов.

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

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
public class MyFacadeClass extends MyTestedClass {
    private MyTestedClass parent;
    private String a;
    private String b;
    private String result;
  
    public MyFacadeClass(MyTestedClass parent) {
        this.parent = parent;
    }
  
    public String getA() {
        return a;
    }
  
    public String getB() {
        return b;
    }
  
    public String getResult() {
        return result;
    }
  
    /**
     * Wrap tested method so we can capture input and output.
     */
    public String op(String a, String b) {
        this.a = a;
        this.b = b;
        String result = parent.op(a, b);
        this.result = result;
        return result;
    }
}

Мы регистрируем основную информацию, как и раньше, и добавляем немного нового кода для регистрации входов и выходов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class AdvancedTestListener extends SimpleTestListener {
 
    AdvancedTestListener() {
    }
 
    /**
     * Log information as before but also log call details.
     */
    public void endTest(Test test) {
        super.endTest(test);
 
        // add captured inputs and outputs
        if (test instanceof MyAdvancedTest) {
            MyTestedClass obj = ((MyAdvancedTest) test).obj;
            if (obj instanceof MyFacadeClass) {
                MyFacadeClass facade = (MyFacadeClass) obj;
                System.out.printf("[, , %s, %s, %s]\n", facade.getA(), facade.getB(), facade.getResult());
            }
        }
    }
}

Журналы теперь показывают как основную информацию, так и детали звонка.

1
2
3
4
5
6
7
8
[MyAdvancedTest, test2, 8/3/15 12:13 AM, 33, null, true]
[, , null, b, :b]
[MyAdvancedTest, test3, 8/3/15 12:13 AM, 0, null, true]
[, , a, null, a:]
[MyAdvancedTest, test4, 8/3/15 12:13 AM, 0, null, true]
[, , null, null, :]
[MyAdvancedTest, test1, 8/3/15 12:13 AM, 0, null, true]
[, , a, b, a:b]

Мы хотим связать базовую информацию и детали вызова, но это легко сделать, добавив уникальный идентификатор теста.

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

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

1
2
3
4
5
6
7
8
[MyAdvancedTest, test2, 8/3/15 12:13 AM, 33, null, true]
{"a":null, "b":"b", "results":":b" }
[MyAdvancedTest, test3, 8/3/15 12:13 AM, 0, null, true]
{"a":"a", "b":null, "results":"a:" }
[MyAdvancedTest, test4, 8/3/15 12:13 AM, 0, null, true]
{"a":null, "b":null, "results":":" }
[MyAdvancedTest, test1, 8/3/15 12:13 AM, 0, null, true]
{"a":" a", "b":"b", "results":" a:b" }

Сбор информации assertX

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

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

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

Во-вторых, нам нужно обернуть методы TestCase assertX, чтобы мы зафиксировали недавние вызовы методов и значения, переданные в вызов assert (плюс, конечно, результаты).

пример

Процесс проще всего показать — и снести — на примере. Давайте начнем с простого POJO.

1
2
3
4
5
6
7
public class Person {
    private String firstName;
    private String lastName;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
}

В этом случае нашему фасаду нужно только записать имя метода.

Типичный метод испытаний

1
2
3
4
5
public void test1() {
    Person p = getTestPerson();
    assertEquals("John", p.getFirstName());
    assertEquals("Smith", p.getLastName());
}

с завернутым методом assertX

01
02
03
04
05
06
07
08
09
10
11
static PersonFacade person;
 
public static void assertEquals(String expected, String actual) {
    // ignoring null handling...
    boolean results = expected.equals(actual);
    LOG.log("assertEquals('" + expected + "',"+person.getMethodsCalled()+ ") = " + results);
    person.clearMethodsCalled();
    if (!results) {
        throw new AssertionFailedError("Expected same:<" + expected + " > was not:<" + actual + ">");
    }
}

так что мы бы получили результаты, как

1
2
assertEquals('John', getFirstName()) = true;
assertEquals('Smith', getLastName()) = false;

Нетрудно понять, как это может быть проанализировано тестовой средой, но это слишком рано для празднования. Второй метод испытаний

1
2
3
4
public void test1() {
    Person p = getTestPerson();
    assertEquals("john", p.getFirstName().toLowerCase());
}

и наш простой код не будет захватывать toLowerCase () . Наш журнал будет ошибочно записывать:

1
assertEquals('John', getFirstName()) = false;

Более патологический случай:

1
2
3
4
5
public void test1() {
    Person p = getTestPerson();
    LOG.log("testing " + p.getFirstName());
    assertEquals("john", "joe");
}

где утверждение не имеет ничего общего с обернутым классом.

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