Alon S
Alon S

Reputation: 195

Mocking a class with internal SDK calls using NSubstitute

First time trying to use NSubstitute.

I have the following method in my Web API. For those who don't know Couchbase, lets say that a collection/bucket is like a DB table and a key is like a DB row.

Couchbase_internal.Collection_GET returns Task<ICouchbaseCollection>

I would like to write 2 unit tests.

One that tests the returned class when the key exist and one when it doesn't (couchbaseServiceResultClass).

I don't really understand where is the part where I control whether or not the key exist in the mocked data.

public class CouchbaseAPI : ControllerBase, ICouchbaseAPI
{

    // GET /document_GET?bucketName=<bucketName>&key=<key>
    [HttpGet]
    [Consumes("application/x-www-form-urlencoded")]
    [Produces(MediaTypeNames.Application.Json)]
    public async Task<couchbaseServiceResultClass> document_GET([FromQuery, BindRequired] string bucketName, [FromQuery, BindRequired] string key)
    {
       

        var collection = await Couchbase_internal.Collection_GET(bucketName);

        if (collection != null)
        {
            IGetResult result;
            try
            {
                // get document
                result = await collection.GetAsync(key);
            }
            catch (CouchbaseException ex)
            {
                return new ErrorHandling().handleCouchbaseException(ex);
            }


            couchbaseServiceResultClass decryptResult = new();

            try
            {
                // decrypt document
                decryptResult = Encryption.decryptContent(result);
            }
            catch (Exception ex)
            {
                return new ErrorHandling().handleException(ex, null);
            }


            // remove document if decryption failed
            if (!decryptResult.DecryptSuccess)
            {
                try
                {
                    await collection.RemoveAsync(key);
                }
                catch (CouchbaseException ex)
                {
                    return new ErrorHandling().handleCouchbaseException(ex);
                }

            }

            decryptResult.Message = "key retrieved successfully";
            // return result
            return decryptResult;

        }
        else
        {
            return new ErrorHandling().handleError("Collection / bucket was not found.");

        }
    }
}

This is what I have so far for the first test:

public class CouchbaseAPITests
{
    
    private readonly CouchbaseAPI.Controllers.ICouchbaseAPI myClass = Substitute.For<CouchbaseAPI.Controllers.ICouchbaseAPI>();


    [Fact]
    public async Task document_GET_aKeyIsRetrievedSuccessfully()
    {

        // Arrange
        string bucketName = "myBucket";
        string keyName = "myKey";            

        couchbaseServiceResultClass resultClass = new();
        resultClass.Success = true;
        resultClass.Message = "key retrieved successfully";


        myClass.document_GET(bucketName, keyName).Returns(resultClass);

        // Act
        var document = await myClass.document_GET(bucketName, keyName);


        // Assert
        Assert.True(document.Success);
        Assert.Equal("key retrieved successfully", document.Message);

    }
}

Upvotes: 0

Views: 406

Answers (1)

David Tchepak
David Tchepak

Reputation: 10484

If we want to test that we are retrieving documents from the Couchbase API properly, then generally we want to use a real instance (local test setup) of that API where possible. If we are mocking this then our tests are not really telling us about whether our code is working correctly (just that our mock is working the way we want it to).

When certain APIs are difficult to use real instances for (e.g. non-deterministic code, difficult to reproduce conditions such as network errors, slow dependencies, etc), that's when it can be useful to introduce an interface for that dependency and to mock that for our test.

Here's a very rough example that doesn't quite match the code snippets posted, but hopefully will give you some ideas on how to proceed.

public interface IDataAdapter {
    IEnumerable<IGetResult> Get(string key);
}

public class CouchbaseAdapter : IDataAdapter {
    /* Implement interface for Couchbase */
}

public class AppApi {
    private IDataAdapter data;
    public AppApi(IDataAdapter data) {
        this.data = data;
    }
    public SomeResult Lookup(string key) {
        try {
            var result = data.Get(key);
            return Transform(Decrypt(result));
        } catch (Exception ex) { /* error handling */ }
    }
}

[Fact]
public void TestWhenKeyExists() {
    var testAdapter = Substitute.For<IDataAdapter>();
    var api = new AppApi(testAdapter);
    testAdapter.Get("abc").Returns(/* some valid data */);

    var result = api.Lookup("abc");

    /* assert that result is decrypted/transformed as expected */
    Assert.Equal(expectedResult, result);
}

[Fact]
public void TestWhenKeyDoesNotExist() {
    var testAdapter = Substitute.For<IDataAdapter>();
    var api = new AppApi(testAdapter);
    var emptyData = new List<IGetResult>();
    testAdapter.Get("abc").Returns(emptyData);

    var result = api.Lookup("abc");

    /* assert that result has handled error as expected */
    Assert.Equal(expectedError, result);
}

Here we've introduced a IDataAdapter type that our class uses to abstract the details of which implementation we are using to get data. Our real code can use the CouchbaseAdapter implementation, but our tests can use a mocked version instead. For our tests, we can simulate what happens when the data adapter throws errors or returns specific information.

Note that we're only testing AppApi here -- we are not testing the CouchbaseAdapter implementation, only that AppApi will respond in a certain way if its IDataAdapter has certain behaviour. To test our CouchbaseAdapter we will want to use a real instance, but we don't have to worry about those details for testing our AppApi transformation and decryption code.

Upvotes: 0

Related Questions