Статьи

Автоматическое обнаружение ошибок с помощью git bisect и mvn test

Знаете ли вы чувство, когда вы обнаружили ошибку в функционале, который работал пару недель (или версий) назад? Жаль, что у нас не было никаких автоматических тестов, и то, что раньше было хорошо, теперь сломано. Давайте возьмем этот простой репозиторий в качестве примера:

1

Сначала напишите тест

Мы заметили, что некоторые конкретные функции были в порядке в версии 1.0, но не работает в 1.1. Что мы делаем в первую очередь? Конечно, напишите тестовый пример, чтобы убедиться, что эта ошибка, однажды исправленная, никогда не вернется! Написание (провал) теста для каждой найденной ошибки имеет много преимуществ:

  1. Он документирует ошибки и доказывает, что они были исправлены
  2. Неочевидные обходные пути и решения не будут удалены («почему он здесь проверяет null ?! Это невозможно, давайте упростим это») случайно
  3. Вы постепенно улучшаете общее покрытие кода, даже в устаревшей кодовой базе.

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

2

Интерактивный ребазинг

Может быть, вместо того, чтобы делать тест после версии 1.1 (где мы знаем, что он сломан), нам следует сделать патч или спрятать этот тест? Таким образом, мы могли бы пройти через все ревизии между 1.0 и 1.1, распаковать или применить патч с test и запустить его. Надеюсь, вы согласны, что это далеко не идеально. Первый трюк заключается в использовании интерактивной перебазировки для того, чтобы сдвинуть коммит с неудачным тестом в прошлое. Однако мы не хотим перебазировать master ветку, поэтому мы делаем временную копию и перебазируем ее:

1
2
3
4
5
$ git checkout -b tmp
Switched to a new branch 'tmp'
  
$ git rebase -i 1.0 tmp
Successfully rebased and updated refs/heads/tmp.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
pick 10dbcc9 Feature 2
pick f4cf58a Feature 3
pick 8287434 Feature 4
pick e79d56f Feature 5
pick 50614b6 Feature 6
pick 21ae08f Feature 7
pick 1e5b5a5 Feature 8
pick f703abf Feature 9
pick 686d7a9 Feature 10
pick b5b5cf1 Feature 11
pick 8e58593 Feature 12
pick 3ab419a Feature 13
pick 0e769a0 Feature 14
pick 8bfdbea Feature 15
pick 0a95b7f Feature 16
pick 4622cbc Feature 17
pick 757c4eb Feature 18
pick 3d94d7e Feature 19
pick da69f6a Feature 20
pick 733bd17 Test for bug #123

Теперь наш репозиторий должен выглядеть примерно так:

3

мерзавец

Самое главное, наш тестовый пример теперь внедряется сразу после версии 1.0 (известно, что это хорошо). Все, что нам нужно сделать, это проверить все ревизии одну за другой и запустить этот тест. СТОП! Если вы сообразительны (или ленивы), вы начнете с коммита прямо в середине, а если он сломан, вы продолжите с первой половиной таким же образом — или возьмите вторую половину иначе. Это как бинарный поиск. Однако отслеживать, какой коммит в последний раз считался хорошим и плохим, а также вручную проверять ревизию в середине, довольно громоздко. К счастью, git может сделать это для нас с помощью команды git bisect . В принципе после начала деления пополам мы указываем последний известный хороший и первый известный плохой коммит. Git проверит ревизию между ними и спросит нас, хорошо это или плохо, продолжая до тех пор, пока мы точно не выясним, какой именно код зафиксирован. В нашем случае мы просто запускаем mvn test и действуем в зависимости от его результата:

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
$ git bisect start
  
$ git bisect good 1.0
  
$ git bisect bad tmp
Bisecting: 9 revisions left to test after this (roughly 3 steps)
[13ed8405beb387ec86874d951cf630de2c4fd927] Feature 10
  
$ mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
  
$ git bisect good
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[b9e610428b61ba1436219edbaa1c5c435a1907ae] Feature 15
  
$ mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
  
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 1 step)
[e8a5ddd4dea219d826a15f7a085e412c29333b10] Feature 17
  
$ mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD FAILURE
  
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[6d974faffa042781a098914a80d962953a492cb5] Feature 16
  
$ mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
  
$ git bisect good
e8a5ddd4dea219d826a15f7a085e412c29333b10 is the first bad commit
commit e8a5ddd4dea219d826a15f7a085e412c29333b10
Author: Tomasz Nurkiewicz
Date:   Wed Mar 19 19:43:40 2014 +0100
  
    Feature 17
  
:100644 100644 469c856b4ede8 90d6b2233832 M      SomeFile.java

Посмотрите, как мы итеративно называем git good / bad выполняя наш тестовый пример между ними? Также обратите внимание, как быстро количество коммитов для тестирования сокращается. Вы можете подумать, что это аккуратно и быстро (логарифмическое время!), Но на самом деле мы можем идти намного быстрее. git bisect есть скрытый драгоценный камень, называемый режимом run . Вместо того, чтобы полагаться на ручной ответ пользователя после каждой итерации, мы можем предоставить скрипт, который сообщает, является ли данная ревизия хорошей или плохой. По соглашению, если этот скрипт завершает работу с кодом 0, это означает успех, тогда как любой другой код завершения сигнализирует об ошибке. К счастью, скрипт mvn следует этому соглашению, поэтому мы можем просто выполнить git bisect mvn test -Dcom.nurkiewicz.BugTest , расслабиться и расслабиться:

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
$ git bisect start
  
$ git bisect good 1.0
  
$ git bisect bad tmp
Bisecting: 9 revisions left to test after this (roughly 3 steps)
[13ed8405beb387ec86874d951cf630de2c4fd927] Feature 10
  
$ git bisect run mvn test -Dcom.nurkiewicz.BugTest
running mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
...
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[b9e610428b61ba1436219edbaa1c5c435a1907ae] Feature 15
running mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
...
Bisecting: 2 revisions left to test after this (roughly 1 step)
[e8a5ddd4dea219d826a15f7a085e412c29333b10] Feature 17
running mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD FAILURE
...
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[6d974faffa042781a098914a80d962953a492cb5] Feature 16
running mvn test -Dcom.nurkiewicz.BugTest
...
[INFO] BUILD SUCCESS
...
e8a5ddd4dea219d826a15f7a085e412c29333b10 is the first bad commit
commit e8a5ddd4dea219d826a15f7a085e412c29333b10
Author: Tomasz Nurkiewicz
Date:   Wed Mar 19 19:43:40 2014 +0100
  
    Feature 17
  
:100644 100644 469c856b4ede8 90d6b2233832 M      SomeFile.java
bisect run success

Программа выше не является интерактивной и полностью автоматизирована. git, после нескольких итераций, указывает, какой именно коммит был первым, кто нарушил тест. Мы можем запустить все тесты, но в этом нет никакого смысла, поскольку мы знаем, что только этот тест не пройден. Конечно, вы можете использовать любую другую команду, а не mvn . Вы даже можете написать какой-нибудь простой скрипт на любом языке JVM по вашему выбору (используйте System.exit () ). git bisect в сочетании с интерактивным перебазированием — замечательные инструменты для поиска регрессий и ошибок. Также они способствуют автоматизированному тестированию и автоматизации в целом.