From a0aea741f72aa4387202a337fc3390f5c481715d Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 3 Jan 2023 12:20:06 -0300 Subject: [PATCH] Write black-box tests for DefaultAbly.connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These document the current behaviour of this method. I wrote them having only seen the Ably interface (which DefaultAbly implements), and knowing the dependencies that DefaultAbly has. I intentionally did not look at the code whilst writing these tests because I wanted to limit myself to writing test cases based on scenarios that I could imagine might occur, instead of ones based on logic that I know to exist. Once I’ve written tests like this for the whole class, then I’d like to look at its implementation and add any further test cases for logic that wasn’t clear from the public interface. This is part of #869. --- .../ably/tracking/common/DefaultAblyTests.kt | 526 +++++++++++++++++- .../helper/DefaultAblyTestEnvironment.kt | 75 ++- .../common/helper/DefaultAblyTestScenarios.kt | 254 +++++++++ 3 files changed, 837 insertions(+), 18 deletions(-) create mode 100644 common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestScenarios.kt diff --git a/common/src/test/java/com/ably/tracking/common/DefaultAblyTests.kt b/common/src/test/java/com/ably/tracking/common/DefaultAblyTests.kt index ae00324c7..3b6eda5a1 100644 --- a/common/src/test/java/com/ably/tracking/common/DefaultAblyTests.kt +++ b/common/src/test/java/com/ably/tracking/common/DefaultAblyTests.kt @@ -1,7 +1,8 @@ package com.ably.tracking.common +import com.ably.tracking.ErrorInformation import com.ably.tracking.common.helper.DefaultAblyTestEnvironment -import io.mockk.verify +import com.ably.tracking.common.helper.DefaultAblyTestScenarios import io.mockk.verifyOrder import io.ably.lib.realtime.ChannelState import io.ably.lib.realtime.ConnectionState @@ -13,25 +14,520 @@ import org.junit.Assert import org.junit.Test class DefaultAblyTests { - // This is just an example test to check that the AblySdkRealtime mocks are working correctly. We need to add a full set of unit tests for DefaultAbly; see https://github.com/ably/ably-asset-tracking-android/issues/869 + /* + Observations from writing black-box tests for `connect`: + + - When given a channel in certain states, it seems to fetch the channel’s state more than once. I have not tested what happens if a different state is returned on the second call. + */ + @Test - fun `connect fetches the channel and then enters presence on it, and when that succeeds the call to connect succeeds`() { - // Given - val testEnvironment = DefaultAblyTestEnvironment.create(numberOfTrackables = 1) - val configuredChannel = testEnvironment.configuredChannels[0] - testEnvironment.mockChannelsContainsKey(key = configuredChannel.channelName, result = false) - testEnvironment.mockChannelsGet(DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS) - configuredChannel.mockSuccessfulPresenceEnter() + fun `connect - when channel fetched is in INITIALIZED state`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the INITIALIZED state... + * ...which, when told to enter presence, does so successfully, + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.initialized, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in ATTACHED state`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the ATTACHED state... + * ...which, when told to enter presence, does so successfully, + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.attached, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in ATTACHING state`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the ATTACHING state... + * ...which, when told to enter presence, does so successfully... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.attaching, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in DETACHING state`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the DETACHING state... + * ...which, when told to enter presence, does so successfully... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ - // When runBlocking { - testEnvironment.objectUnderTest.connect(configuredChannel.trackableId, PresenceData("")) + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.detaching, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) } + } - // Then - verify { - testEnvironment.channelsMock.get(configuredChannel.channelName, any()) - configuredChannel.presenceMock.enter(any(), any()) + @Test + fun `connect - when presence enter fails`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the (arbitrarily-chosen) INITIALIZED state... + * ...which, when told to enter presence, fails to do so with an arbitrarily-chosen error `presenceError`... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and releases the channel... + * ...and the call to `connect` (on the object under test) fails with a ConnectionException whose errorInfo has the same `code` and `message` as `presenceError`. + */ + + val presenceError = ErrorInfo( + "example of an error message", /* arbitrarily chosen */ + 123 /* arbitrarily chosen */ + ) + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.initialized, /* arbitrarily chosen */ + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Failure( + presenceError + ), + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = true, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.FailureWithConnectionException( + ErrorInformation( + presenceError.code, + 0, + presenceError.message, + null, + null + ) + ) + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in FAILED state and attach succeeds`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the FAILED state... + * ...which, when told to enter presence, does so successfully... + * ...and which, when told to attach, does so successfully... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to attach... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.failed, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + verifyChannelAttach = true, + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in FAILED state and attach fails`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the FAILED state... + * ...which, when told to attach, fails to do so with an arbitrarily-chosen error `attachError`... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to attach... + * ...and releases the channel... + * ...and the call to `connect` (on the object under test) fails with a ConnectionException whose errorInfo has the same `code` and `message` as `attachError`. + */ + + val attachError = ErrorInfo( + "example of an error message", /* arbitrarily chosen */ + 123 /* arbitrarily chosen */ + ) + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.failed, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Failure( + attachError + ), + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = false, + verifyChannelAttach = true, + verifyChannelRelease = true, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.FailureWithConnectionException( + ErrorInformation(attachError.code, 0, attachError.message, null, null) + ), + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in DETACHED state and attach succeeds`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the DETACHED state... + * ...which, when told to enter presence, does so successfully... + * ...and which, when told to attach, does so successfully... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state once... + * ...and tells the channel to attach... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.detached, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 1, + verifyChannelAttach = true, + verifyPresenceEnter = true, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in DETACHED state and attach fails`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the DETACHED state... + * ... which, when told to attach, fails to do so with an arbitrarily-chosen error... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state once... + * ...and tells the channel to attach... + * ...and releases the channel... + * ...and the call to `connect` (on the object under test) fails with a ConnectionException whose errorInfo has the same `code` and `message` as `attachError`. + */ + + val attachError = ErrorInfo( + "example of an error message", /* arbitrarily chosen */ + 123 /* arbitrarily chosen */ + ) + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.detached, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Failure( + attachError + ), + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 1, + verifyChannelAttach = true, + verifyPresenceEnter = false, + verifyChannelRelease = true, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.FailureWithConnectionException( + ErrorInformation(attachError.code, 0, attachError.message, null, null) + ) + ) + ) + } + } + + @Test + fun `connect - when channel fetched is in SUSPENDED state`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns false... + * ...and that calling `get` (the overload that accepts a ChannelOptions object) on the Channels instance returns a channel in the SUSPENDED state... + * ...which, when told to enter presence, does so successfully... + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that accepts a ChannelOptions object) on the Channels instance... + * ...and checks the channel’s state 2 times... + * ...and tells the channel to enter presence... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = false, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + channelState = ChannelState.suspended, + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 2, + verifyPresenceEnter = true, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) + } + } + + @Test + fun `connect - when channel already exists`() { + /* Given... + * + * ...that calling `containsKey` on the Channels instance returns true... + * ...and that calling `get` (the overload that does not accept a ChannelOptions object) on the Channels instance returns a channel in the (arbitrarily-chosen) INITIALIZED state... + * ...which, when told to enter presence, does so successfully, + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload that does not accept a ChannelOptions object) on the Channels instance... + * ...and the call to `connect` (on the object under test) succeeds. + */ + + runBlocking { + DefaultAblyTestScenarios.Connect.test( + DefaultAblyTestScenarios.Connect.GivenConfig( + channelsContainsKey = true, + channelsGetOverload = DefaultAblyTestEnvironment.ChannelsGetOverload.WITHOUT_CHANNEL_OPTIONS, + channelState = ChannelState.initialized, /* arbitrarily chosen */ + presenceEnterBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.Success, + channelAttachBehaviour = DefaultAblyTestScenarios.CompletionListenerMockBehaviour.NotMocked, + ), + DefaultAblyTestScenarios.Connect.ThenConfig( + overloadOfChannelsGetToVerify = DefaultAblyTestEnvironment.ChannelsGetOverload.WITHOUT_CHANNEL_OPTIONS, + numberOfChannelStateFetchesToVerify = 0, + verifyPresenceEnter = false, + verifyChannelAttach = false, + verifyChannelRelease = false, + resultOfConnectCallOnObjectUnderTest = DefaultAblyTestScenarios.Connect.ThenConfig.ConnectResult.Success + ) + ) } } diff --git a/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestEnvironment.kt b/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestEnvironment.kt index dc760abb6..2548904af 100644 --- a/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestEnvironment.kt +++ b/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestEnvironment.kt @@ -13,6 +13,7 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.excludeRecords import io.mockk.confirmVerified +import io.mockk.verifySequence import io.ably.lib.realtime.ConnectionState import io.ably.lib.realtime.ConnectionStateListener import io.ably.lib.types.ErrorInfo @@ -103,16 +104,34 @@ class DefaultAblyTestEnvironment private constructor( } /** - * Mocks [presenceMock]’s [AblySdkRealtime.Presence.enter] method to immediately call its received completion listener’s [CompletionListener.onSuccess] method. + * Mocks [presenceMock]’s [AblySdkRealtime.Presence.enter] method to immediately pass its received completion listener to [handler]. + * + * @param handler The function that should receive the completion listener passed to [presenceMock]’s [AblySdkRealtime.Presence.enter] method. */ - fun mockSuccessfulPresenceEnter() { + private fun mockPresenceEnterResult(handler: (CompletionListener) -> Unit) { val completionListenerSlot = slot() every { presenceMock.enter( any(), capture(completionListenerSlot) ) - } answers { completionListenerSlot.captured.onSuccess() } + } answers { handler(completionListenerSlot.captured) } + } + + /** + * Mocks [presenceMock]’s [AblySdkRealtime.Presence.enter] method to immediately call its received completion listener’s [CompletionListener.onSuccess] method. + */ + fun mockSuccessfulPresenceEnter() { + mockPresenceEnterResult { it.onSuccess() } + } + + /** + * Mocks [presenceMock]’s [AblySdkRealtime.Presence.enter] method to immediately call its received completion listener’s [CompletionListener.onError] method. + * + * @param errorInfo The error that should be passed to the completion listener’s [CompletionListener.onError] method. + */ + fun mockFailedPresenceEnter(errorInfo: ErrorInfo) { + mockPresenceEnterResult { it.onError(errorInfo) } } /** @@ -154,6 +173,36 @@ class DefaultAblyTestEnvironment private constructor( fun mockNonCompletingPresenceLeave() { mockPresenceLeaveResult { } } + + /** + * Mocks [channelMock]’s [AblySdkRealtime.Channel.attach] method to immediately pass its received completion listener to [handler]. + * + * @param handler The function that should receive the completion listener passed to [channelMock]’s [AblySdkRealtime.Channel.attach] method. + */ + private fun mockAttachResult(handler: (CompletionListener) -> Unit) { + val completionListenerSlot = slot() + every { channelMock.attach(capture(completionListenerSlot)) } answers { + handler( + completionListenerSlot.captured + ) + } + } + + /** + * Mocks [channelMock]’s [AblySdkRealtime.Channel.attach] method to immediately call its received completion listener’s [CompletionListener.onSuccess] method. + */ + fun mockSuccessfulAttach() { + mockAttachResult { it.onSuccess() } + } + + /** + * Mocks [channelMock]’s [AblySdkRealtime.Channel.attach] method to immediately call its received completion listener’s [CompletionListener.onError] method. + * + * @param errorInfo The error that should be passed to the completion listener’s [CompletionListener.onError] method. + */ + fun mockFailedAttach(errorInfo: ErrorInfo) { + mockAttachResult { it.onError(errorInfo) } + } } /** @@ -216,6 +265,26 @@ class DefaultAblyTestEnvironment private constructor( every { channelsMock.containsKey(key) } returns result } + /** + * An array of all of the MockK mock objects that this test environment has created. + * + * By passing this array to [confirmVerified], you can confirm that the mock verifications that your test makes using MockK’s `verify*` methods contain a comprehensive list of all the methods called on the mocks created by this test environment. This is a more comprehensive approach than using, for example, [verifySequence], which only verifies the mock objects which are mentioned inside its `verifyBlock`. + */ + val allMocks: Array + get() { + val list = listOf( + realtimeMock, + connectionMock, + channelsMock + ) + configuredChannels.flatMap { + listOf( + it.channelMock, + it.presenceMock + ) + } + return list.toTypedArray() + } + /** * Mocks [channelsMock]’s [AblySdkRealtime.Channels.entrySet] method to return the list of channel mocks currently contained in [configuredChannels]. */ diff --git a/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestScenarios.kt b/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestScenarios.kt new file mode 100644 index 000000000..528bce9ba --- /dev/null +++ b/common/src/test/java/com/ably/tracking/common/helper/DefaultAblyTestScenarios.kt @@ -0,0 +1,254 @@ +package com.ably.tracking.common.helper + +import com.ably.tracking.ConnectionException +import com.ably.tracking.ErrorInformation +import com.ably.tracking.common.DefaultAbly +import com.ably.tracking.common.PresenceData +import io.ably.lib.realtime.ChannelState +import io.ably.lib.types.ErrorInfo +import io.mockk.confirmVerified +import io.mockk.verifyOrder +import org.junit.Assert + +class DefaultAblyTestScenarios { + sealed class CompletionListenerMockBehaviour() { + object NotMocked : CompletionListenerMockBehaviour() + object Success : CompletionListenerMockBehaviour() + class Failure(val errorInfo: ErrorInfo) : CompletionListenerMockBehaviour() + } + + /** + * Provides test scenarios for [DefaultAbly.connect]. See the [Companion.test] method. + */ + class Connect { + /** + * This class provides properties for configuring the "Given" stage of the parameterised test case described by [Companion.test]. See that method’s documentation for information about the effect of this class’s properties. + */ + class GivenConfig( + val channelsContainsKey: Boolean, + val channelsGetOverload: DefaultAblyTestEnvironment.ChannelsGetOverload, + val channelState: ChannelState, + val presenceEnterBehaviour: CompletionListenerMockBehaviour, + val channelAttachBehaviour: CompletionListenerMockBehaviour + ) + + /** + * This class provides properties for configuring the "Then" stage of the parameterised test case described by [Companion.test]. See that method’s documentation for information about the effect of this class’s properties. + */ + class ThenConfig( + val overloadOfChannelsGetToVerify: DefaultAblyTestEnvironment.ChannelsGetOverload, + val numberOfChannelStateFetchesToVerify: Int, + val verifyPresenceEnter: Boolean, + val verifyChannelAttach: Boolean, + val verifyChannelRelease: Boolean, + val resultOfConnectCallOnObjectUnderTest: ConnectResult + ) { + /** + * Describes the expected result of the []The effect of the values of this class is described in the documentation for [Companion.test]. + */ + sealed class ConnectResult() { + object Success : ConnectResult() + class FailureWithConnectionException(val errorInformation: ErrorInformation) : + ConnectResult() + } + } + + companion object { + /** + * Implements the following parameterised test case for [DefaultAbly.connect]: + * + * ```text + * Given... + * + * ...that calling `containsKey` on the Channels instance returns ${givenConfig.channelsContainsKey}... + * ...and that calling `get` (the overload described by ${givenConfig.channelsGetOverload}) on the Channels instance returns a channel in the ${givenConfig.channelState} state... + * + * (when ${givenConfig.presenceEnterBehaviour} is Success) { + * ...which, when told to enter presence, does so successfully... + * } + * + * (when ${givenConfig.presenceEnterBehaviour} is Failure) { + * ...which, when told to enter presence, fails to do so with error ${givenConfig.presenceEnterBehaviour.errorInfo}... + * } + * + * (when ${givenConfig.channelAttachBehaviour} is Success) { + * ...[and] which, when told to attach, does so successfully... + * } + * + * (when ${givenConfig.channelAttachBehaviour} is Failure) { + * ...[and] which, when told to attach, fails to do so with error ${givenConfig.channelAttachBehaviour.errorInfo}... + * } + * + * When... + * + * ...we call `connect` on the object under test, + * + * Then... + * ...in the following order, precisely the following things happen... + * + * ...it calls `containsKey` on the Channels instance... + * ...and calls `get` (the overload described by ${givenConfig.channelsGetOverload}) on the Channels instance... + * ...and checks the channel’s state ${thenConfig.numberOfChannelStateFetchesToVerify} times... + * + * (if ${thenConfig.verifyChannelAttach}) { + * ...and tells the channel to attach... + * } + * + * (if ${thenConfig.verifyPresenceEnter}) { + * ...and tells the channel to enter presence... + * } + * + * (if ${thenConfig.verifyChannelRelease}) { + * ...and releases the channel... + * } + * + * (when ${thenConfig.resultOfConnectCallOnObjectUnderTest} is Success) { + * ...and the call to `connect` (on the object under test) succeeds. + * } + * + * (when ${thenConfig.resultOfConnectCallOnObjectUnderTest} is FailureWithConnectionException) { + * ...and the call to `connect` (on the object under test) fails with a ConnectionException whose errorInfo is equal to ${thenConfig.resultOfConnectCallOnObjectUnderTest.errorInfo}. + * } + * ``` + * + * @param givenConfig Parameters for the "Given..." part of the test case. + * @param thenConfig Parameters for the "Then..." part of the test case. + */ + suspend fun test( + givenConfig: GivenConfig, + thenConfig: ThenConfig + ) { + // Given... + // ...that calling `containsKey` on the Channels instance returns ${givenConfig.channelsContainsKey}... + // ...and that calling `get` (the overload described by ${givenConfig.channelsGetOverload}) on the Channels instance returns a channel in the ${givenConfig.channelState} state... + val testEnvironment = DefaultAblyTestEnvironment.create(numberOfTrackables = 1) + val configuredChannel = testEnvironment.configuredChannels[0] + testEnvironment.mockChannelsContainsKey( + key = configuredChannel.channelName, + result = givenConfig.channelsContainsKey + ) + testEnvironment.mockChannelsGet(givenConfig.channelsGetOverload) + configuredChannel.mockState(givenConfig.channelState) + + testEnvironment.stubRelease(configuredChannel) + + when (val givenPresenceEnterBehaviour = givenConfig.presenceEnterBehaviour) { + is CompletionListenerMockBehaviour.NotMocked -> {} + /* (when ${givenConfig.presenceEnterBehaviour} is Success) { + * ...which, when told to enter presence, does so successfully... + * } + */ + is CompletionListenerMockBehaviour.Success -> { + configuredChannel.mockSuccessfulPresenceEnter() + } + /* (when ${givenConfig.presenceEnterBehaviour} is Failure) { + * ...which, when told to enter presence, fails to do so with error ${givenConfig.presenceEnterBehaviour.errorInfo}... + * } + */ + is CompletionListenerMockBehaviour.Failure -> { + configuredChannel.mockFailedPresenceEnter(givenPresenceEnterBehaviour.errorInfo) + } + } + + when (val givenChannelAttachBehaviour = givenConfig.channelAttachBehaviour) { + is CompletionListenerMockBehaviour.NotMocked -> {} + /* (when ${givenConfig.channelAttachBehaviour} is Success) { + * ...[and] which, when told to attach, does so successfully... + * } + */ + is CompletionListenerMockBehaviour.Success -> { + configuredChannel.mockSuccessfulAttach() + } + /* (when ${givenConfig.channelAttachBehaviour} is Failure) { + * ...[and] which, when told to attach, fails to do so with error ${givenConfig.channelAttachBehaviour.errorInfo}... + * } + */ + is CompletionListenerMockBehaviour.Failure -> { + configuredChannel.mockFailedAttach(givenChannelAttachBehaviour.errorInfo) + } + } + + // When... + + // ...we call `connect` on the object under test, + val result = testEnvironment.objectUnderTest.connect( + configuredChannel.trackableId, + PresenceData("") + ) + + // Then... + // ...in the following order, precisely the following things happen... + verifyOrder { + // ...it calls `containsKey` on the Channels instance... + testEnvironment.channelsMock.containsKey(configuredChannel.channelName) + + // ...and calls `get` (the overload described by ${thenConfig.overloadOfChannelsGetToVerify}) on the Channels instance... + when (thenConfig.overloadOfChannelsGetToVerify) { + DefaultAblyTestEnvironment.ChannelsGetOverload.WITHOUT_CHANNEL_OPTIONS -> { + testEnvironment.channelsMock.get(configuredChannel.channelName) + } + DefaultAblyTestEnvironment.ChannelsGetOverload.WITH_CHANNEL_OPTIONS -> { + testEnvironment.channelsMock.get(configuredChannel.channelName, any()) + } + } + + // ...and checks the channel’s state ${thenConfig.numberOfChannelStateFetchesToVerify} times... + repeat(thenConfig.numberOfChannelStateFetchesToVerify) { + configuredChannel.channelMock.state + } + + if (thenConfig.verifyChannelAttach) { + /* (if ${thenConfig.verifyChannelAttach}) { + * ...and tells the channel to attach... + * } + */ + configuredChannel.channelMock.attach(any()) + } + + if (thenConfig.verifyPresenceEnter) { + /* (if ${thenConfig.verifyPresenceEnter}) { + * ...and tells the channel to enter presence... + * } + */ + configuredChannel.presenceMock.enter(any(), any()) + } + + if (thenConfig.verifyChannelRelease) { + /* (if ${thenConfig.verifyChannelRelease}) { + * ...and releases the channel... + * } + */ + testEnvironment.channelsMock.release(configuredChannel.channelName) + } + } + + when (val thenResultOfConnectCallOnObjectUnderTest = + thenConfig.resultOfConnectCallOnObjectUnderTest) { + is ThenConfig.ConnectResult.Success -> { + /* (when ${thenConfig.resultOfConnectCallOnObjectUnderTest} is Success) { + * ...and the call to `connect` (on the object under test) succeeds. + * } + */ + Assert.assertTrue(result.isSuccess) + } + is ThenConfig.ConnectResult.FailureWithConnectionException -> { + /* (when ${thenConfig.resultOfConnectCallOnObjectUnderTest} is FailureWithConnectionException) { + * ...and the call to `connect` (on the object under test) fails with a ConnectionException whose errorInfo is equal to ${thenConfig.resultOfConnectCallOnObjectUnderTest.errorInfo}. + * } + */ + Assert.assertTrue(result.isFailure) + val exception = result.exceptionOrNull()!! + Assert.assertTrue(exception is ConnectionException) + val connectionException = exception as ConnectionException + Assert.assertEquals( + connectionException.errorInformation, + thenResultOfConnectCallOnObjectUnderTest.errorInformation + ) + } + } + + confirmVerified(*testEnvironment.allMocks) + } + } + } +}