How To Mock MongoDB SDK Cursor/Methods For Unit Testing DAL In C#

MongoDB is a widely used database and we usually come across situations where we have to unit test functions in DAL by mocking MongoDB SDK methods. It is little tricky when you want to mock the cursors and write the unit test cases.

Below are few samples on how to Mock MongoDB SDK and write unit tests in C# and XUnit and Moq.

Class to be Unit tested

Consider the below class to be unit tested which is a DAL layer. IMongoClient(which is a singleton defined in startup of .net core) is injected into the constructor. Database and collection instances are initiated in the constructor from IMongoClient.

GetProducts is a method that retrieves the list of products matching the input criteria using aggregation pipeline(utilizing MongoDB Atlas search). Ignore the complex pipeline here. I used it to show why Unit testing DAL methods are very important.

public class MongoDBService {
    private readonly IMongoCollection < Products > productCollection;
    public MongoDBService(IMongoClient mongoClient) {
        var mongoDB = mongoClient.GetDatabase("databaseName");
        this.productCollection = mongoDB.GetCollection < Products > ("collectionName");
    }
    public async Task < List < Products >> GetProducts(SearchRequestDTO searchRequestDTO) {
        //Aggregation pipeline stages construction starts.
        var departmentfilter = new BsonDocument {
            {
                MongoDBConstants.QueryOperator, searchRequestDTO.Department
            }, {
                MongoDBConstants.PathOperator,
                ProductInfoConstants.Department
            }
        };
        var categories = new BsonArray();
        categories.AddRange(searchRequestDTO.Categories);
        var categoryFilter = new BsonDocument {
            {
                MongoDBConstants.QueryOperator, categories
            }, {
                MongoDBConstants.PathOperator,
                ProductInfoConstants.Categories
            }
        };
        var must = new BsonArray();
        must.Add(new BsonDocument {
            {
                MongoDBConstants.PhraseOperator, departmentfilter
            }
        });
        must.Add(new BsonDocument {
            {
                MongoDBConstants.PhraseOperator, categoryFilter
            }
        });
        foreach(var attribute in searchRequestDTO.AttributeFilter) {
            string path = string.Empty;
            ProductInfoConstants.Attributes.TryGetValue(attribute.K, out path);
            var attributefilter = new BsonDocument {
                {
                    MongoDBConstants.QueryOperator, attribute.V
                }, {
                    MongoDBConstants.PathOperator,
                    path
                }
            };
            must.Add(new BsonDocument {
                {
                    MongoDBConstants.PhraseOperator, attributefilter
                }
            });
        }
        var compound = new BsonDocument {
            {
                MongoDBConstants.MustOperator, must
            }
        };
        var search = new BsonDocument {
            {
                MongoDBConstants.IndexKey, MongoDBConstants.DefaultIndexvalue
            }, {
                MongoDBConstants.CompoundOperator,
                compound
            }
        };
        var searchStage = new BsonDocument {
            {
                MongoDBConstants.SearchOperator, search
            }
        };
        var pipeline = new [] {
            searchStage
        };
        //aggregation pipeline stages construction ends.
        var result = await this.productCollection.AggregateAsync < Products > (pipeline);
        return await result.ToListAsync();
    }
}

In the above method, below line of code makes a database call to MongoDB that needs to be mocked.

await this.productCollection.AggregateAsync<Products>(pipeline)

Unit Test Step

To achieve the same and write unit test for GetProcuts Method using C3, XUnit, Moq, let's follow the steps below.

Step 1

We have to mock IMongoClient, IMongoDatabase, IMongoCollection<Products>, IAsyncCursor<Products>, and sample data of product collection. I have injected only one product here. You can add as many test data as you want.

Note: Products is the data model of the collection.

public class MongoDBServiceUnitTest {
    private Mock < IMongoClient > mongoClient;
    private Mock < IMongoDatabase > mongodb;
    private Mock < IMongoCollection < Products >> productCollection;
    private List < Products > productList;
    private Mock < IAsyncCursor < Products >> productCursor;
    public MongoDBServiceUnitTest() {
        this.settings = new MongoDBSettings("ecommerce-db");
        this.mongoClient = new Mock < IMongoClient > ();
        this.productCollection = new Mock < IMongoCollection < Products >> ();
        this.mongodb = new Mock < IMongoDatabase > ();
        this.productCursor = new Mock < IAsyncCursor < Products >> ();
        var product = new Products {
            Id = "1",
                Attributes = new Dictionary < string, string > () {
                    {
                        "ram",
                        "4gb"
                    }, {
                        "diskSpace",
                        "128gb"
                    }
                },
                Categories = new List < string > () {
                    "Mobiles"
                },
                Department = "Electronics",
                Description = "Brand new affordable samsung mobile",
                Item = "Samsung Galaxy M31s",
                Sku = "sku1234567",
                Quantity = 99,
                Image = "url",
                InsertedDate = DateTime.UtcNow,
                UpdatedDate = DateTime.UtcNow,
                SchemaVersion = 1,
                ManufactureDetails = new ManufactureDetails() {
                    Brand = "Samsung",
                        Model = "M31s"
                },
                Pricing = new Pricing() {
                    Price = 15000,
                        Currency = "INR",
                        Discount = 1000,
                        DiscountExpireAt = DateTime.UtcNow.AddDays(10)
                },
                Rating = new Rating() {
                    AggregateRating = 4.3,
                        TotalReviews = 10000,
                        Stars = new List < int > () {
                            1,
                            2,
                            3,
                            4,
                            5
                        }
                }
        };
        this.productList = new List < Products > () {
            product
        };
    }
}

Step 2

Add private method below to setup IMongoDatabase and IMongoClient when getcollection and getdatabase are invoked.

Setup IMongoDatabase first before setup IMongoClient that returns IMongoDatabase object.

private void InitializeMongoDb() {
    this.mongodb.Setup(x => x.GetCollection < Products > (MongoDBConstants.ProductCollectionName,
        default)).Returns(this.productCollection.Object);
    this.mongoClient.Setup(x => x.GetDatabase(It.IsAny < string > (),
        default)).Returns(this.mongodb.Object);
}

Step 3

Add private method below to setup the cursor that returns productList. Then Mock the Aggregationpipeline result using the product cursor.

private void InitializeMongoProductCollection() {
    this.productCursor.Setup( => .Current).Returns(this.productList);
    this.productCursor.SetupSequence( => .MoveNext(It.IsAny < CancellationToken > ())).Returns(true).Returns(false);
    this.productCursor.SetupSequence( => .MoveNextAsync(It.IsAny < CancellationToken > ())).Returns(Task.FromResult(true)).Returns(Task.FromResult(false));
    this.productCollection.Setup(x => x.AggregateAsync(It.IsAny < PipelineDefinition < Products, Products >> (), It.IsAny < AggregateOptions > (), It.IsAny < CancellationToken > ())).ReturnsAsync(this.productCursor.Object);
    this.InitializeMongoDb();
}

Step 4

Now we have all setup complete. we can write the unit case for GetProducts.

[Fact]
public async Task GetProductsValidataData() {
    this.InitializeMongoProductCollection();
    var mongoDBService = new MongoDBService(this.settings, this.mongoClient.Object);
    var response = await mongoDBService.GetProducts(new Models.DTO.Request.SearchRequestDTO() {
        Department = "any",
            Categories = new List < string > () {
                "any"
            },
            AttributeFilter = new List < Models.DTO.Request.AttributeFilterDTO > () {
                new Models.DTO.Request.AttributeFilterDTO() {
                    K = "size",
                        V = "3"
                }
            }
    });
    // do assertion of the result from GetProducts.
}

Like AggregateAsync method, We can mock any other method in MongoDB library for our unit testing.