夜神模拟器模拟安卓测试
即使自从我开始为Grails应用程序编写测试以来,我也找不到很多关于使用模拟的文章。 每个人都在谈论测试和TDD,但是如果您搜索它,则文章不多。
今天,我想与您分享一个简单完整的模拟测试。 我有一个简单的应用程序,可以获取Twitter推文并将其呈现给用户。 我使用REST服务,并使用GET通过ID来获取推文,例如: http : //api.twitter.com/1/statuses/show/236024636775735296.json 。 您可以将其复制并粘贴到浏览器中以查看结果。
我的应用程序使用带有spock-0.6的Grails 2.1进行测试。 我有TwitterReaderService通过id来获取推文,然后将响应解析到我的Tweet类中。
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 action和tweet的id一起调用。 此动作是我的测试对象。 我已经实现了一些基本功能。 编写测试时更容易专注于此。
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注入控制器:
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这一行最重要:
Tweet readTweet(String id) throws TwitterError
您现在必须将这种方法想像成黑盒子。 从控制器的角度来看,您对内部构件一无所知。 您只对可以为您退还的款项感兴趣:
- 可以引发TwitterError
- 可以返回null
- Tweet实例可以返回
此列表是您的测试蓝图。 现在,为每个元素回答一个简单的问题:“在这种情况下,我想让我的控制器做什么?” 并且您有计划测试:
- 如果抛出TwitterError并提示错误,则show操作应重定向到索引
- show action应该重定向到索引并通知是否未找到tweet
- show动作应该显示找到推文
那很简单明了! 现在是最好的部分:我们使用twitterReaderServiceMock来模拟这三种情况中的每一种!
在Spock中,有一个很好的文档介绍了与模拟的交互 。 您可以声明调用的方法,调用的次数,指定的参数以及应返回的参数。 还记得黑匣子吗? 模拟是您的黑匣子,上面有详细的说明,例如: 我希望您,如果只收到一个调用参数为'1'的readTweet ,则应该向我抛出TwitterError 。 将此句子大声改写,然后看一下:
1 * twitterReaderServiceMock.readTweet('1') >> { throw new TwitterError() }
这是模拟的有效交互定义! 就这么简单! 这是一个目前失败的完整测试:
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'
}
}
| 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 。
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 * twitterReaderServiceMock.readTweet('1') >> { throw new TwitterError() }
这是对三种方案和通过它的控制器的完整测试。
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
}
}
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]
}
}
这里最重要的是,我们已经测试了控制器与服务之间的交互,而没有在服务中实现逻辑! 这就是模拟技术如此有用的原因。 它消除了您的依赖关系,使您可以专注于测试中的一个主题。 测试愉快!
参考: 如何在我们的JCG合作伙伴 TomaszKalkosiński的refaktor博客上的控制器测试中使用 模拟 。
翻译自: https://www.javacodegeeks.com/2013/07/how-to-use-mocks-in-controller-tests.html
夜神模拟器模拟安卓测试