夜神模拟器模拟安卓测试

即使自从我开始为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

夜神模拟器模拟安卓测试