Kotlin mock aws s3client call with generic callback - unit-testing

I am trying to write unit test that verifies whether my function attempts to request resource from S3 upon receiving S3 notification. Simplified code below:
override fun handleRequest(s3Event: S3Event?, context: Context?): String {
s3Event.records
.map { record -> record.s3.`object`.key }
.forEach { recordKey -> runBlocking { fetchFileFromS3(bucketName, recordKey) } }
return "Success reading files from event!"
}
private suspend fun fetchFileFromS3(bucketName: String, objectKey: String) {
val request = GetObjectRequest {
key = objectKey
bucket = bucketName
}
val response = s3Client.getObject(request) { response -> response }
response.body?.writeToFile(File("output.txt"))
}
My issue is that I cannot succesfully mock call to s3 client - I have tried both Mockk and Mockito Kotlin wrapper.
My first approach was using Mockk library:
#Test
fun shouldFetchS3File_whenS3NotificationReceived(): Unit = runBlocking {
val callback = any<(GetObjectResponse) -> GetObjectResponse>()
coEvery { mockS3Client.getObject(any(), TODO ) } coAnswers { sampleResponse() }
handler.handleRequest(sampleS3Event(), null)
}
I am not a Kotlin expert, but I believe that the issue in this scenario can be somehow related to generic callback included in getObject method:
public suspend fun <T> getObject(input: GetObjectRequest, block: suspend (GetObjectResponse) -> T): T
I always get the same error: io.mockk.MockKException: no answer found for: S3Client(#1).getObject(GetObjectRequest(...), continuation {}, continuation {}), which I belive means that the second argument of getObject method was not matched.
In the second aproach, I have tried using Mockito, and got the test running (failing tho) with the following snippet:
#Test
fun shouldTriggerSES_whenS3NotificationReceived() = runBlocking {
val callback = any<(GetObjectResponse) -> GetObjectResponse>()
mockS3Client.stub {
onBlocking { getObject(any(), callback) }.doReturn(sampleResponse())
}
handler.handleRequest(sampleS3Event(), null)
}
And in this case, there's NPE indicating that 'response' was null (to my understanding the issue was the same - no call was matched, but Mockito does not throw exception in this scenario).
I would greatly appreciate a proper way of mocking such call, no matter the library, with some explanation on matching such generic code blocks.
EDIT:
I am using the following test dependencies:
testImplementation("io.mockk:mockk:1.13.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0")

Related

Ktor+Koin: Why unit test still keep connect to mongodb even I mocked the service that injected with `CoroutineDatabase` instance

Note: I'm a Kotlin beginner
Developing stack
Ktor
Koin
KMongo: MongoDB
I defined my application to be like this
Route -> Service -> Repository
Route is define all HTTP request endpoints.
Service is business logic of application.
Repository is data access layer for query/persist data to MongoDB.
What I did to test a route, if the simple route like.
fun Route.healthCheck() {
get("/health") {
call.respond(HealthCheckResponse(message = "OK"))
}
}
#Test
fun testHealth() = testApplication {
application {
configureRouting()
}
client.get("/health").apply {
expect {
that(status).isEqualTo(HttpStatusCode.OK)
that(contentType()?.contentType).isNotNull().and {
contains(ContentType.Application.Json.contentType)
}
that(Json.decodeFromString(HealthCheckResponse.serializer(), bodyAsText()).message)
.isEqualTo("OK")
}
}
}
Test above will run good.
But when the case of the route that has DI, injecting CoroutineDatabase object into Repository then that repository inject it to Service and the service inject into Route.
In unit test code, I defined like below.
// Route define
fun Application.configureRouting() {
routing {
user()
}
}
fun Route.user() {
val userService: UserService by inject()
...
}
class UserServiceImpl(
private val userRepository: UserRepository // <- Repository is injected with MongoDB `CoroutineDatabase` object.
) : AccountService {
...
}
===============
// Unit Test
class UserEndpointTest: KoinTest {
#get:Rule
val koinTestRule = KoinTestRule.create {
modules(module{ single { accountService } })
}
#Test
fun testUserEndpoint() = testApplication {
application {
configureRouting() // -> collecting all extended function of `Route`
}
val client = createClient {
install(ContentNegotiation) {
json()
}
}
val testerEmail = "i_am_testing#the-email.dev"
val requestJson = """
{
"email": "$testerEmail",
"password": "aBCdEf9"
}
""".trimIndent()
val testBody = jsonMapper.decodeFromString(CreateAccountRequest.serializer(), requestJson)
coEvery { mockAccountService.submitUser(any()) } returns User(id = newId(), email = testerEmail, password = "")
client.post("$accountEndpoint/user") {
contentType(ContentType.Application.Json)
setBody(testBody)
}.apply {
expect {
that(status).isEqualTo(HttpStatusCode.Created)
that(contentType()?.contentType).isNotNull().and {
contains(ContentType.Application.Json.contentType)
}
that((body() as AccountResponse).id).isNotBlank()
that((body() as AccountResponse).email).isNotBlank().and {
isEqualTo(testerEmail)
}
}
}
}}
I expected I did mocking service and it should be inject into Route then it won't chaining call to repository and CoroutineDatabase object.
But when I run it keep connecting to database, I noticed with log below.
2022-09-07 02:51:30.093 [cluster-ClusterId{value='631788a0b273ac122eaa8350', description='null'}-localhost:27017] DEBUG org.mongodb.driver.cluster - Updating cluster description to {type=UNKNOWN, servers=[{address=localhost:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
And I tried to find out a tutorial to find how to write Unit Test with scenario like these, but I found most of answers on searched results are before Ktor version up. (Noticed from their codes still using withTestApplication {} which is deprecated already in version 2.1.0
Who can explain the step of Koin DI work and how to interrupt it with mock.
Thanks.

How to test code before a suspended coroutine function

Consider the following example in a view model class:
override fun signInViewSelected(
email: String,
password: String
) {
viewModelScope.launch {
loadingViewState.value = LoadingState.Loading("Loading")
withContext(dispatcher.ioDispatcher) {
authManager.signIn(email, password) // suspend fun signIn(email: String, password: String): Boolean -> makes network call
}
loadingViewState.value = LoadingState.NotLoading
}
}
How could I test this method so that I can verify that I start out in loading state, call the authManager.signIn method, and then end up in the not loading state?
When I had this setup with completion handlers I was able to capture the argument passed to my mock authManager class, and then call that manually to advance the completion, but with coroutines I'm not familiar with how to do the equivalent behavior.
What I'd want is something like this, ideally:
#Test
fun `sign in loading state`() {
signInViewModel.signInViewSelected("email#email.com", "password")
val inProgressLoadingViewState = signInViewModel.loadingViewState.getOrAwaitValue()
assertLoadingStateIsLoading(inProgressLoadingViewState, progressMessage)
// delay mockAuthManager until now, have it execute at this point
val finishedLoadingViewState = signInViewModel.loadingViewState.getOrAwaitValue()
assertLoadingStateIsNotLoading(finishedLoadingViewState)
}
Any thoughts?
Assuming you're familiar with libraries like AssertJ and Mockito, I would go about it the following way:
First off, mock the observer of loadingViewState (assuming you have some LiveData in place) and authManager:
private lateinit var viewModel: SomeViewModel
private val loadingViewStateObserver = mock<Observer<LoadingViewState>>()
fun initViewModel() {
viewModel = SomeViewModel().apply {
loadingViewStateLiveData.observeForever(loadingViewStateObserver)
}
}
#Test
fun `sign in loading state`() {
runBlocking {
initViewModel()
viewModel.signInViewSelected("email", "password")
inOrder(loadingViewStateObserver, authManager) {
verify(loadingViewStateObserver).onChanged(LoadingState.NotLoading)
verify(authManager).signIn("email", "password")
verify(loadingViewStateObserver).onChanged(LoadingState.Loading("Loading"))
}
}
}

How to write unit tests for asynchronous function with a callback argument

I'm writing common tests for my kotlin multiplatform library which implements the API business logic using ktor client library.
I have a function which takes a callback as an argument, use coroutines to make the request to the API, and then execute the callback.
Here is a simplified version of the function from my UserApi class I want to test
fun <T : Any> fetch(
requestBuilder: HttpRequestBuilder,
deserializer: DeserializationStrategy<T>,
callback: (Either<ErrorMessage, T>) -> Unit)
{
GlobalScope.launch(dispatcherIO) {
val result: Either<ErrorMessage, T> =
try {
val returnObject: T = Json.parse(
deserializer,
HttpClient().post(requestBuilder)
)
Either.Right(returnObject)
} catch (e: Exception) {
Either.Left(ErrorMessage(e.message))
}
withContext(dispatcherMain) { callback(result) }
}
}
I would like to write a unit test like that:
#Test
fun requestOK() {
runTest { //runTest returns a platform specific runBlocking
val UserApi().fetch(request, User.serializer()) {
it.fold(
{ failure -> fail("must return success" },
{ user -> assertEquals(expectedUser, user) }
)
}
}
}

Unit Test NService.Send from an API Controller

I have an API Controller which publishes a command using NServiceBus. I am using NUnit and NSubstitute for testing. I want to test that certain properties from the model are populated on the command
Here is my controller with a route.
[RoutePrefix("api/fileService")]
public class FileServiceController : ApiController
{
[HttpPost]
[Route("releasefile")]
public async Task<IHttpActionResult> ReleaseFile(FileReleaseAPIModels.ReleaseFileModel model)
{
var currentUser = RequestContext.Principal?.Identity as ClaimsIdentity;
if (model.FileType.Equals("ProductFile"))
{
_logger.Info($"Releasing Product files for date: {model.FileDate.ToShortDateString()} ");
_bus.Send<IReleaseProductFiles>("FileManager.Service", t =>
{
t.FileId = Guid.NewGuid();
t.RequestedDataDate = model.FileDate;
t.RequestingUser = currentUser?.Name;
t.RequestDateTime = DateTime.Now;
});
}
return Ok();
}
}
In my test, I substitute(mock) Ibus and try to validate the call received. Here is the test method:
[Test]
public async Task TestReleaseProductsFile()
{
var bus = Substitute.For<IBus>();
var dbContent = _container.Resolve<IFileManagerDbContext>();
var apiContext = new FileServiceController(bus, dbContent);
//Create a snapshot
var releaseDate = DateTime.Now.Date;
var result = await apiContext.ReleaseFile(new ReleaseFileModel
{
FileDate = releaseDate,
FileType = "ProductFile"
});
Assert.That(result, Is.Not.Null, "Result is null");
Assert.That(result, Is.TypeOf<OkResult>(), "Status code is not ok");
bus.Received(1)
.Send<IReleaseProductFiles>(Arg.Is<string>("FileManager.Service"), Arg.Is<Action<IReleaseProductFiles>>(
action =>
{
action.FileId = Guid.NewGuid();
action.RequestedDataDate = releaseDate;
action.RequestingUser = String.Empty;
action.RequestDateTime = DateTime.Now;
}));
}
This results in error - even though the message is actually sent. Here is the error message:
NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching:
Send<IReleaseProductFiles>("Capelogic.Service", Action<IReleaseProductFiles>)
Actually received no matching calls.
Received 1 non-matching call (non-matching arguments indicated with '*' characters):
Send<IReleaseProductFiles>("Capelogic.Service", *Action<IReleaseProductFiles>*)
I am obviously missing something obvious here.
The problem here is with the Action<IReleaseProductFiles> argument to Send -- we can't automatically tell if two different actions are the same. Instead, NSubstitute relies on the references being equivalent. Because both the test and the production code create their own Action instance, these will always be different and NSubstitute will say the calls don't match.
There are a few different options for testing this. These examples relate to Expression<Func<>>, but the same ideas apply to Action<>s.
In this case I'd be tempted to test this indirectly:
[Test]
public async Task TestReleaseProductsFile()
{
var bus = Substitute.For<IBus>();
var returnedProductFiles = Substitute.For<IReleaseProductFiles>();
// Whenever bus.Send is called with "FileManager.Service" arg, invoke
// the given callback with the `returnedProductFiles` object.
// We can then make sure the action updates that object as expected.
bus.Send<IReleaseProductFiles>(
"FileManager.Service",
Arg.Invoke<IReleaseProductFiles>(returnedProductFiles));
// ... remainder of test ...
Assert.That(result, Is.TypeOf<OkResult>(), "Status code is not ok");
Assert.That(returnedProductFiles.FileId, Is.Not.EqualTo(Guid.Empty));
Assert.That(returnedProductFiles.RequestedDataDate, Is.EqualTo(releaseDate));
Assert.That(returnedProductFiles.RequestingUser, Is.EqualTo(String.Empty));
}
I'd recommend having a look through the previously mentioned answer though to see if there is a better fit for your situation.

How to mock spray-client response

I have a simple spray client :
val pipeline = sendReceive ~> unmarshal[GoogleApiResult[Elevation]]
val responseFuture = pipeline {Get("http://maps.googleapis.com/maps/api/elevation/jsonlocations=27.988056,86.925278&sensor=false") }
responseFuture onComplete {
case Success(GoogleApiResult(_, Elevation(_, elevation) :: _)) =>
log.info("The elevation of Mt. Everest is: {} m", elevation)
shutdown()
case Failure(error) =>
log.error(error, "Couldn't get elevation")
shutdown()
}
Full code can be found here.
I want to mock the response of the server to test the logic in the Success and Failure cases. The only relevant information i found was here but I haven't been able to use the cake pattern to mock the sendReceive method.
Any suggestion or example would be greatly appreciated.
Here's an example of one way to mock it using specs2 for the test spec and mockito for the mocking. First, the Main object refactored into a class setup for mocking:
class ElevationClient{
// we need an ActorSystem to host our application in
implicit val system = ActorSystem("simple-spray-client")
import system.dispatcher // execution context for futures below
val log = Logging(system, getClass)
log.info("Requesting the elevation of Mt. Everest from Googles Elevation API...")
import ElevationJsonProtocol._
import SprayJsonSupport._
def sendAndReceive = sendReceive
def elavation = {
val pipeline = sendAndReceive ~> unmarshal[GoogleApiResult[Elevation]]
pipeline {
Get("http://maps.googleapis.com/maps/api/elevation/json?locations=27.988056,86.925278&sensor=false")
}
}
def shutdown(): Unit = {
IO(Http).ask(Http.CloseAll)(1.second).await
system.shutdown()
}
}
Then, the test spec:
class ElevationClientSpec extends Specification with Mockito{
val mockResponse = mock[HttpResponse]
val mockStatus = mock[StatusCode]
mockResponse.status returns mockStatus
mockStatus.isSuccess returns true
val json = """
{
"results" : [
{
"elevation" : 8815.71582031250,
"location" : {
"lat" : 27.9880560,
"lng" : 86.92527800000001
},
"resolution" : 152.7032318115234
}
],
"status" : "OK"
}
"""
val body = HttpEntity(ContentType.`application/json`, json.getBytes())
mockResponse.entity returns body
val client = new ElevationClient{
override def sendAndReceive = {
(req:HttpRequest) => Promise.successful(mockResponse).future
}
}
"A request to get an elevation" should{
"return an elevation result" in {
val fut = client.elavation
val el = Await.result(fut, Duration(2, TimeUnit.SECONDS))
val expected = GoogleApiResult("OK",List(Elevation(Location(27.988056,86.925278),8815.7158203125)))
el mustEqual expected
}
}
}
So my approach here was to first define an overridable function in the ElevationClient called sendAndReceive that just delegates to the spray sendReceive function. Then, in the test spec, I override that sendAndReceive function to return a function that returns a completed Future wrapping a mock HttpResponse. This is one approach for doing what you want to do. I hope this helps.
There's no need to introduce mocking in this case, as you can simply build a HttpResponse much more easily using the existing API:
val mockResponse = HttpResponse(StatusCodes.OK, HttpEntity(ContentTypes.`application/json`, json.getBytes))
(Sorry for posting this as another answer, but don't have enough karma to comment)