Reputation: 171
I have a method below which is pretty straightforward. It calls another method that soft deletes an API key and then calls another method to create a new one and returns it.
Test is below also which just checks that the two methods were called correctly. But still getting 0 invocation error on both methods though. What causes this problem?
AuthApiKeyPair updateApiKeyPair(AuthApiKeyPair apiKeyPair, Boolean createNewKey) {
AuthApiKeyPair newKeyPair
if (createNewKey) {
deleteApiKeyPair(apiKeyPair)
//The key will be created with the same info as the previous key.
newKeyPair = createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)
}
newKeyPair
}
TEST:
def "should soft delete key pair and create new one"() {
setup:
AuthApiKeyPair apiKeyPair = AuthApiKeyPair.build(acquirerId: 123, source: PaymentSource.BOARDING_API, label: 'Boarding API key')
when:
service.updateApiKeyPair(apiKeyPair, true)
then:
1 * service.deleteApiKeyPair(apiKeyPair)
1 * service.createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)
}
Upvotes: 4
Views: 3621
Reputation: 42174
If you think about your test you will realize that in best case scenario it tests Spock's mocking mechanism and not your business code. You haven't shown us full class with your test specification, however basing on your scenario we can assume that service
in your test is simply a mock. If this is true, then you can't expect these two invocations:
then:
1 * service.deleteApiKeyPair(apiKeyPair)
1 * service.createApiKeyPair(apiKeyPair.label, apiKeyPair.accountMode, apiKeyPair.source)
to happen. Simply because mocked class does not execute real methods.
I would strongly suggest you testing a real class and not what kind of invocations specific method causes, but what are the expected (and deterministic) results of invoking specific method(s) instead. You could execute service.updateApiKeyPair(apiKeyPair, true)
on a real object in when
clause and then you could check if the new API key pair was created (and persisted in storage you use) and if the old pair does not exist anymore. Such test has at least a few benefits over checking invocations only:
service.updateApiKeyPair()
at any time and as long as it produces the same results your test is still useful (because the test does not limit your implementation like the invocation test does),Of course it may require some design changes. I'm guessing that your service class uses some injected DAO or repository that persists API key pairs. Consider providing an in-memory implementation of such DAO for your test - a class that instead of persisting objects in a real database stores all objects in the internal ConcurrentMap<K,V>
. Thanks to this you can still run your test as a unit test and you can test if updating API key pair with createNewKey
parameter set to true
does exactly what you expect. Alternatively you could write an integration test with H2 database replacement, but it only makes your test bootstrap much longer. The choice is yours.
There is one rule worth remembering - if your class/component/functionality etc. is hard to unit test, it means that there were made a bad design choices.
Spy
objectsThere is one thing I mention in the end on purpose. Spock supports so called "spy" objects, that behave like a real objects, but they allow you to stub some parts and treat this object like a mock for e.g. invocation counting. But even Spock authors caution developers about using this feature:
(Think twice before using this feature. It might be better to change the design of the code under specification.)
Source: http://spockframework.org/spock/docs/1.0/interaction_based_testing.html#spies
I don't know if Grails has an annotation for a test to create Spy instead of a Mock, but you can always follow official documentation and create plain Spock unit test that instantiates your service as a Spy and then you can try counting invocations. I wouldn't suggest doing this though, just mentioning this for the record.
Upvotes: 8