Azure / azure-cosmos-dotnet-v2

Contains samples and utilities relating to the Azure Cosmos DB .NET SDK
MIT License
579 stars 837 forks source link

Ensure SDK is mockable for UnitTesting #342

Open yyanev opened 7 years ago

yyanev commented 7 years ago

I am trying to create a IDocumentClient mock but ran into a problem. While IDocumentQuery is public and allows mocking, IDocumentQueryProvider is internal. We have calls to query.CountAsync(), which is implemented in DocumentQueryable like this:

return ((IDocumentQueryProvider)source.Provider).ExecuteAsync(...

here "source" is our mock implementation of IDocumentQuery but source.Provider is not IDocumentProvider and the mock cannot be completed.

The same problem applies to all other aggregate functions,

kirankumarkolli commented 7 years ago

@yyanev for mocking we need to look at it holistically across all SDK code. I will mark this for backlog.

MovGP0 commented 6 years ago

I have the same problem now. I would need to create a DocumentQuery, but I am unable to do so, because DocumentQuery and DocumentQueryable.CreateDocumentQuery are both marked as internal:

internal sealed class DocumentQuery<T> 
   : IDocumentQuery<T>, IDocumentQuery, IDisposable, IOrderedQueryable<T>, IEnumerable<T>, IEnumerable, IOrderedQueryable, IQueryable, IQueryable<T> { /* ... */ }
mattfrear commented 6 years ago

Same boat here. Since changing my production code to access RequestCharge and ETag in the ResourceResponse, my test code blows up with NullReferenceExceptions

eanyanwu commented 6 years ago

For anyone else who runs into the issue of mocking a ResourceResponse, you can use reflection as a temporary workaround. Using the CosmosDB Emulator was not a viable option for us. Tests go to go fast. 🐎

You can use the following helper method to create fake ResourceResponse objects with specific status codes and Request Charges. (With slight modifications, you can also set various other properties). You'll then use some sort of simple mocking to have the mocked DocumentClient return this fake response.

@mattfrear , I don't think ResourceResponse has an Etag property. Maybe you meant Document?

/// <summary>
/// Various parts of the Azure DocumentDb SDK are not mockable due to missing constructors/read-only properties. 
/// To get around this. we use reflection to create mocks.
/// ---
/// Create a resource response from a document using reflection
/// </summary>
/// <param name="resource">The Document</param>
/// <param name="statusCode">The status code this resource response should have.</param>
/// <returns></returns>
private ResourceResponse<Document> CreateResourceResponse(Document resource, HttpStatusCode statusCode)
{
    // Skeleton of the ResourceResponse object we want to create. 
    // It doesn't have setters for properties like StatusCode and Request Charge.
    // To set them, we need to access it's internal DocumentServiceResponse property 
    // and set them there.
    var resourceResponse = new ResourceResponse<Document>(resource);

    // Get the DocumentServiceResponse Type object
    var documentServiceResponseType = Type.GetType("Microsoft.Azure.Documents.DocumentServiceResponse, Microsoft.Azure.Documents.Client, Version=1.19.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

    var flags = BindingFlags.NonPublic | BindingFlags.Instance;

    // The constructor of DocumentServiceResponse (internal property of ResourceResponse)
    // accepts as one of its arguments, a name-value collection.
    // Some of ResourceResponse properties are retrieved via
    // the contents of this name-value collection.
    var headers = new NameValueCollection();

    // Add the request charge to the header.
    // If you need to mock the request charge for some reason, you can pass it into
    // this helper method and set it here.
    headers.Add("x-ms-request-charge", "0");

    // The status code property is set in the constructor, 
    // so include it as an argument that we will pass to the constructor.
    var arguments = new object[] { Stream.Null, headers, statusCode, null };

    // This will invoke the DocumentServiceResponse constructor and set the status code. 
    // Since the request charge is in the header name-value collection, 
    // it is now available for "getting" from the enclosing ResourceResponse object
    var documentServiceResponse = Activator.CreateInstance(documentServiceResponseType, flags, null, arguments, null);

    // Get the DocumentServiceResponse field in the ResourceResponse<Document> Type
    var responseField = typeof(ResourceResponse<Document>).GetField("response", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

    // Set it! phew.
    responseField.SetValue(resourceResponse, documentServiceResponse);

    return resourceResponse;
}

Edit: The names of the keys in the key-value collection can be found by de-compiling the Microsoft.Azure.Documents.Client dll

DOliana commented 6 years ago

the same goes for FeedResponse

christothes commented 6 years ago

In addition, IDocumentClient should describe all the methods defined on DocumentClient concrete class.

joshidp commented 6 years ago

Hi @eanyanwu

Thanks for your solution. When I tried using it I am getting constructor not found error at below line. var documentServiceResponse = Activator.CreateInstance(documentServiceResponseType, flags, null, arguments, null);

Assembly Details:

  Name Value Type
Assembly {Microsoft.Azure.Documents.Client, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35} System.Reflection.Assembly {System.Reflection.RuntimeAssembly}

Error: System.MissingMethodException: 'Constructor on type 'Microsoft.Azure.Documents.DocumentServiceResponse' not found.'

Please suggest what should I change?

Thanks in advance!

joshidp commented 6 years ago

Please respond

Elfocrash commented 6 years ago

You can check some TestingExtensions I wrote @joshidp.

Here is the extension method that convert any object to a ResourceReponse.

public static ResourceResponse<T> ToResourceResponse<T>(this T resource, HttpStatusCode statusCode, IDictionary<string, string> responseHeaders = null) where T : Resource, new()
{
    var resourceResponse = new ResourceResponse<T>(resource);
    var documentServiceResponseType = Type.GetType("Microsoft.Azure.Documents.DocumentServiceResponse, Microsoft.Azure.DocumentDB.Core, Version=1.9.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

    var flags = BindingFlags.NonPublic | BindingFlags.Instance;

    var headers = new NameValueCollection { { "x-ms-request-charge", "0" } };

    if (responseHeaders != null)
    {
        foreach (var responseHeader in responseHeaders)
        {
            headers[responseHeader.Key] = responseHeader.Value;
        }
    }

    var arguments = new object[] { Stream.Null, headers, statusCode, null };

    var documentServiceResponse =
        documentServiceResponseType.GetTypeInfo().GetConstructors(flags)[0].Invoke(arguments);

    var responseField = typeof(ResourceResponse<T>).GetTypeInfo().GetField("response", BindingFlags.NonPublic | BindingFlags.Instance);

    responseField?.SetValue(resourceResponse, documentServiceResponse);

    return resourceResponse;
}

This will only work for pre-2.0.0 SDK versions.

For post 2.0.0 use this one instead.

public static ResourceResponse<T> ToResourceResponse<T>(this T resource, HttpStatusCode statusCode, IDictionary<string, string> responseHeaders = null) where T : Resource, new()
{
    var resourceResponse = new ResourceResponse<T>(resource);
    var documentServiceResponseType = Type.GetType("Microsoft.Azure.Documents.DocumentServiceResponse, Microsoft.Azure.DocumentDB.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

    var flags = BindingFlags.NonPublic | BindingFlags.Instance;

    var headers = new NameValueCollection { { "x-ms-request-charge", "0" } };

    if (responseHeaders != null)
    {
        foreach (var responseHeader in responseHeaders)
        {
            headers[responseHeader.Key] = responseHeader.Value;
        }
    }

    var headersDictionaryType = Type.GetType("Microsoft.Azure.Documents.Collections.DictionaryNameValueCollection, Microsoft.Azure.DocumentDB.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

    var headersDictionaryInstance = Activator.CreateInstance(headersDictionaryType, headers);

    var arguments = new [] { Stream.Null, headersDictionaryInstance, statusCode, null };

    var documentServiceResponse = documentServiceResponseType.GetTypeInfo().GetConstructors(flags)[0].Invoke(arguments);

    var responseField = typeof(ResourceResponse<T>).GetTypeInfo().GetField("response", flags);

    responseField?.SetValue(resourceResponse, documentServiceResponse);

    return resourceResponse;
}

You can read more about CosmosDB C# code unit testing here

joshidp commented 6 years ago

Thanks @Elfocrash for the reply.

I tried the solution you gave for 2.0.0 version (released 4 days back). But I am getting null value for below Get Type calls


var documentServiceResponseType = Type.GetType("Microsoft.Azure.Documents.DocumentServiceResponse, Microsoft.Azure.DocumentDB.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

var headersDictionaryType = Type.GetType("Microsoft.Azure.Documents.Collections.DictionaryNameValueCollection, Microsoft.Azure.DocumentDB.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

I have matched the KeyTOken and version with original dll and it's correct.

Please suggest what am I missing.

Thanks

Elfocrash commented 6 years ago

@joshidp Are you using Microsoft.Azure.DocumentDB or Microsoft.Azure.DocumentDB.Core? if you are using the first one you need to change Microsoft.Azure.DocumentDB.Core to Microsoft.Azure.DocumentDB

joshidp commented 6 years ago

Thanks, I have corrected the assembly name to non-core. It's working fine now.

Thanks a lot :)

iqsarv commented 5 years ago

Hi,

Any update on this?

is there a in-memory version of IDocumentClient similar to https://github.com/aws-samples/aws-dynamodb-examples/blob/master/src/test/java/com/amazonaws/services/dynamodbv2/local/embedded/DynamoDBEmbeddedTest.java

It will great to have such implementation to test basic flow

leixnj commented 4 years ago

I am trying to create a IDocumentClient mock but ran into a problem. While IDocumentQuery is public and allows mocking, IDocumentQueryProvider is internal. We have calls to query.CountAsync(), which is implemented in DocumentQueryable like this:

return ((IDocumentQueryProvider)source.Provider).ExecuteAsync(...

here "source" is our mock implementation of IDocumentQuery but source.Provider is not IDocumentProvider and the mock cannot be completed.

The same problem applies to all other aggregate functions,

@yyanev did you solve this issue? the issue was created in 2017. :(

emmet-m commented 4 years ago

+1 on this still being an issue, mocking anything that returns a ResourceResponse<T> is impossible without runtime reflections!

j82w commented 4 years ago

This is fixed in the v3 SDK which is open source on github.