Даже с тех пор, как я начал писать тесты для своего приложения 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.TestForimport 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экземпляр может быть возвращен
Этот список — ваш тестовый проект. Теперь ответьте на простой вопрос для каждого элемента: «Что я хочу, чтобы мой контроллер делал в этой ситуации?» и у вас есть план теста:
-
showaction должен перенаправить на индекс, еслиTwitterErrorброшен и сообщить об ошибке -
showaction должен перенаправить на индекс и сообщить, если твит не найден -
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.TestForimport 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.TestForimport 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] }} |
Самое главное, что мы протестировали взаимодействие между контроллером и сервисом без реализации логики в сервисе! Вот почему имитирующая техника так полезна. Он разъединяет ваши зависимости и позволяет сосредоточиться только на одном тестируемом предмете. Удачного тестирования!