Даже с тех пор, как я начал писать тесты для своего приложения Grails, я не мог найти много статей об использовании макетов. Все говорят о тестах и TDD, но если вы ищете их, статей не так много.
Сегодня я хочу поделиться с вами тестом с макетами для простого и полного сценария. У меня есть простое приложение, которое может получить твиты Twitter и представить его пользователю. Я использую сервис REST и использую GET для получения твитов по идентификатору, например: http://api.twitter.com/1/statuses/show/236024636775735296.json . Вы можете скопировать и вставить его в свой браузер, чтобы увидеть результат.
Мое приложение использует Grails 2.1 с spock-0.6 для тестов. У меня есть TwitterReaderService
который извлекает твиты по идентификатору, а затем анализирует ответ в своем классе твитов.
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
|
class TwitterReaderService { Tweet readTweet(String id) throws TwitterError { try { String jsonBody = callTwitter(id) Tweet parsedTweet = parseBody(jsonBody) return parsedTweet } catch (Throwable t) { throw new TwitterError(t) } } private String callTwitter(String id) { // TODO: implementation } private Tweet parseBody(String jsonBody) { // TODO: implementation } } class Tweet { String id String userId String username String text Date createdAt } class TwitterError extends RuntimeException {} |
TwitterController
играет главную роль здесь. Пользователи вызывают действие show
вместе с id
твита. Это действие — мой тестируемый предмет. Я реализовал некоторые основные функции. На этом легче сосредоточиться при написании тестов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class TwitterController { def twitterReaderService def index() { } def show() { Tweet tweet = twitterReaderService.readTweet(params.id) if (tweet == null ) { flash.message = 'Tweet not found' redirect(action: 'index' ) return } [tweet: tweet] } } |
Давайте начнем писать тест с нуля. Самое главное, что я использую макет для моего TwitterReaderService
. Я не new TwitterReaderService()
, потому что в этом тесте я тестирую только TwitterController
. Я не заинтересован в инъекционном обслуживании. Я знаю, как эта служба должна работать, и я не интересуюсь внутренним оборудованием. Поэтому перед каждым тестом я twitterReaderServiceMock
в контроллер:
01
02
03
04
05
06
07
08
09
10
11
|
import grails.test.mixin.TestFor import spock.lang.Specification @TestFor (TwitterController) class TwitterControllerSpec extends Specification { TwitterReaderService twitterReaderServiceMock = Mock(TwitterReaderService) def setup() { controller.twitterReaderService = twitterReaderServiceMock } } |
Теперь пришло время подумать, какие сценарии мне нужно протестировать. Эта строка из TwitterReaderService
является наиболее важной:
1
|
Tweet readTweet(String id) throws TwitterError |
Вы должны думать об этом методе как о черном ящике прямо сейчас. Вы ничего не знаете о внутренностях с точки зрения диспетчера. Вы интересуетесь только тем, что может быть возвращено вам:
-
TwitterError
-
null
может быть возвращен -
Tweet
экземпляр может быть возвращен
Этот список — ваш тестовый проект. Теперь ответьте на простой вопрос для каждого элемента: «Что я хочу, чтобы мой контроллер делал в этой ситуации?» и у вас есть план теста:
-
show
action должен перенаправить на индекс, еслиTwitterError
брошен и сообщить об ошибке -
show
action должен перенаправить на индекс и сообщить, если твит не найден -
show
действие должно показать найденный твит
Это было легко и просто! А теперь лучшая часть: мы используем twitterReaderServiceMock
чтобы высмеивать каждый из этих трех сценариев!
В Споке есть хорошая документация по взаимодействию с макетами . Вы объявляете, какие методы вызываются, сколько раз, какие параметры передаются и что должны быть возвращены. Помните черный ящик? Макет — это ваш черный ящик с подробными инструкциями, например: я ожидаю, что если вы получите ровно один вызов readTweet
с параметром ‘1’, вы должны выбросить мне TwitterError
. Перефразируйте это предложение вслух и посмотрите на это:
1
|
1 * twitterReaderServiceMock.readTweet( '1' ) >> { throw new TwitterError() } |
Это правильное определение взаимодействия на макете! Это так просто! Вот полный тест, который пока не проходит:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import grails.test.mixin.TestFor import spock.lang.Specification @TestFor (TwitterController) class TwitterControllerSpec extends Specification { TwitterReaderService twitterReaderServiceMock = Mock(TwitterReaderService) def setup() { controller.twitterReaderService = twitterReaderServiceMock } def "show should redirect to index if TwitterError is thrown" () { given: controller.params.id = '1' when: controller.show() then: 1 * twitterReaderServiceMock.readTweet( '1' ) >> { throw new TwitterError() } 0 * _._ flash.message == 'There was an error on fetching your tweet' response.redirectUrl == '/twitter/index' } } |
1
2
3
|
| Failure: show should redirect to index if TwitterError is thrown(pl.refaktor.twitter.TwitterControllerSpec) | pl.refaktor.twitter.TwitterError at pl.refaktor.twitter.TwitterControllerSpec.show should redirect to index if TwitterError is thrown_closure1(TwitterControllerSpec.groovy: 29 ) |
Вы можете заметить нотацию 0 * _._
. Это говорит: я не хочу никаких других издевательств или любых других методов, вызываемых. Сбой этого теста, если что-то называется! Это хорошая практика, чтобы убедиться, что нет больше взаимодействий, чем вы хотите.
Хорошо, теперь мне нужно реализовать логику контроллера для обработки TwitterError
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class TwitterController { def twitterReaderService def index() { } def show() { Tweet tweet try { tweet = twitterReaderService.readTweet(params.id) } catch (TwitterError e) { log.error(e) flash.message = 'There was an error on fetching your tweet' redirect(action: 'index' ) return } [tweet: tweet] } } |
Мои тесты проходят! У нас осталось два сценария. Правило остается прежним: TwitterReaderService
возвращает что-то, и мы проверяем это. Так что эта строка является сердцем каждого теста, меняйте только возвращаемые значения после >>
:
1
|
1 * twitterReaderServiceMock.readTweet( '1' ) >> { throw new TwitterError() } |
Вот полный тест для трех сценариев и контроллера, который его проходит.
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
|
import grails.test.mixin.TestFor import spock.lang.Specification @TestFor (TwitterController) class TwitterControllerSpec extends Specification { TwitterReaderService twitterReaderServiceMock = Mock(TwitterReaderService) def setup() { controller.twitterReaderService = twitterReaderServiceMock } def "show should redirect to index if TwitterError is thrown" () { given: controller.params.id = '1' when: controller.show() then: 1 * twitterReaderServiceMock.readTweet( '1' ) >> { throw new TwitterError() } 0 * _._ flash.message == 'There was an error on fetching your tweet' response.redirectUrl == '/twitter/index' } def "show should inform about not found tweet" () { given: controller.params.id = '1' when: controller.show() then: 1 * twitterReaderServiceMock.readTweet( '1' ) >> null 0 * _._ flash.message == 'Tweet not found' response.redirectUrl == '/twitter/index' } def "show should show found tweet" () { given: controller.params.id = '1' when: controller.show() then: 1 * twitterReaderServiceMock.readTweet( '1' ) >> new Tweet() 0 * _._ flash.message == null response.status == 200 } } |
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
|
class TwitterController { def twitterReaderService def index() { } def show() { Tweet tweet try { tweet = twitterReaderService.readTweet(params.id) } catch (TwitterError e) { log.error(e) flash.message = 'There was an error on fetching your tweet' redirect(action: 'index' ) return } if (tweet == null ) { flash.message = 'Tweet not found' redirect(action: 'index' ) return } [tweet: tweet] } } |
Самое главное, что мы протестировали взаимодействие между контроллером и сервисом без реализации логики в сервисе! Вот почему имитирующая техника так полезна. Он разъединяет ваши зависимости и позволяет сосредоточиться только на одном тестируемом предмете. Удачного тестирования!