Home > database >  How to correctly mock mongoDb with XUnit in C#?
How to correctly mock mongoDb with XUnit in C#?

Time:11-15

Problem:

I am new to writing unit testing in c# using xunit. So I am trying to mock the MongoDB connection. In my project I have use repository pattern there I have used unit of work class like this. so I am accessing every repository via it. so the unit of work class code is here.

namespace QuestionBank.API.Repositories
{
    public class UnitOfWork : IUnitOfWork
    {
        public readonly IQuestionsBankDBContext _context;

        private readonly ILogger<UnitOfWork> _logger;

        private Dictionary<Type, object> repositories;

        private IQuestionsRepository _questionsRepository;

        private ICampaignQuestionsRepository _campaignQuestionsRepository;

        private ICandidateAnswerRepository _candidateAnswerRepository;

        private IIntergrationEventLogRepository _integrationEventLogRepository;

        private IControlRepository _controlRepository;


        public UnitOfWork(IQuestionsBankDBContext context, ILogger<UnitOfWork> logger)
        {
            _context = context;
            _logger = logger;
        }

        public IQuestionsRepository QuestionsRepository
        {
            get
            {
                this._questionsRepository = new QuestionsRepository(_context as IQuestionsBankDBContext, this, _logger);
                return this._questionsRepository;
            }
        }

        public ICandidateAnswerRepository CandidateAnswerRepository
        {
            get
            {
                this._candidateAnswerRepository = new CandidateAnswerRepository(_context as IQuestionsBankDBContext, this, _logger);
                return this._candidateAnswerRepository;
            }
        }

        public ICampaignQuestionsRepository CampaignQuestionsRepository
        {
            get
            {
                this._campaignQuestionsRepository = new CampaignQuestionsRepository(_context as IQuestionsBankDBContext, this, _logger);
                return this._campaignQuestionsRepository;
            }
        }

        public IIntergrationEventLogRepository IntegrationEventLogRepository
        {
            get
            {
                this._integrationEventLogRepository = new IntergrationEventLogRepository(_context as IQuestionsBankDBContext, this, _logger);
                return this._integrationEventLogRepository;
            }
        }

        public IControlRepository ControlRepository
        {
            get
            {
                this._controlRepository = new ControlRepository(_context as IQuestionsBankDBContext, this, _logger);
                return this._controlRepository;
            }
        }

        public IGenericRepository<TDocument> GetRepository<TDocument>() where TDocument : IDocument
        {
            if (this.repositories == null)
            {
                this.repositories = new Dictionary<Type, object>();
            }

            var type = typeof(TDocument);
            if (!this.repositories.ContainsKey(type))
            {
                this.repositories[type] = new GenericRepository<TDocument>(_context);
            }

            return (IGenericRepository<TDocument>)this.repositories[type];
        }
    }
}

So in the unit test to mock services and repositories, I need to pass database context to unitofwork. I tried it this way.

var mockDbContext = new Mock<QuestionsBankDBContext>();
var dbContext = mockDbContext.Object;
var mock = new Mock<ILogger<UnitOfWork>>();
_logger = mock.Object;
unitOfWork = new UnitOfWork(dbContext, _logger);

questionsService = new QuestionsService(unitOfWork);
campaignQuestionsService = new CampaignQuestionsService(unitOfWork);
tokenService = new TokenService();

stringLocalizer = new Mock<IStringLocalizer<SharedResource>>();

questionBankIntergrationEventService = new Mock<IQuestionBankIntergrationEventService>();

questionsController = new QuestionsController(questionsService, campaignQuestionsService, stringLocalizer.Object, tokenService, questionBankIntergrationEventService.Object);

contextMock = new Mock<HttpContext>();

And this is my DB context class.

using MongoDB.Driver;
using QuestionBank.API.Models;

namespace QuestionBank.API.Data
{
    public class QuestionsBankDBContext : IQuestionsBankDBContext
    {
        public IMongoClient Client { get; set; }
        public IMongoDatabase Database { get; set; }


        public QuestionsBankDBContext(IQuestionBankDatabaseSettings settings)
        {
            Client = new MongoClient(settings.ConnectionString);
            Database = Client.GetDatabase(settings.DatabaseName);
        }
    }
}

Then I wrote a unit test like this.

[Theory]
[InlineData("61879e54e86be1fa5e41831f")]
[InlineData("61879e54e86be1fa5e41831e")]
public async Task GetQuestionById(string questionId)
{
    var actionResult = await questionsController.GetQuestionById(questionId);
    var result = actionResult as ObjectResult;
    Assert.NotNull(result.Value);
    if (result.StatusCode == (int)System.Net.HttpStatusCode.OK)
    {
        Assert.IsType<Questions>(result.Value);
    }
    else if (result.StatusCode == (int)System.Net.HttpStatusCode.NotFound)
    {
        Assert.Contains("ErrorCode", result.Value.ToString());
    }
    else if (result.StatusCode == (int)System.Net.HttpStatusCode.InternalServerError)
    {
        var code = (int)ErroCodes.InternalServerError;
        Assert.Contains(code.ToString(), result.Value.ToString());
    }
}

Then when running this it gives enter image description here

And my question controller GetQuestionById is like this.

[HttpGet]
//[Authorize(Roles = "SuperAdmin,Admin")]
[Route("getquestionbyidfrombank")]
[ProducesResponseType(typeof(Questions), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
public async Task<IActionResult> GetQuestionById([FromQuery] string questionId)
{
    try
    {
        string errorText;
        if (!string.IsNullOrEmpty(questionId))
        {
            var question = await 
            questionsService.GetQuestionById(questionId);
            return Ok(question);
        }
        else
        {
            errorText = string.Format(stringLocalizer[Constants.ErrorCodeString],
        (int)ErroCodes.SpecifiedItemNotFound,
        stringLocalizer[Helper.ToEnumString(ErroCodes.SpecifiedItemNotFound)]);

            return StatusCode(404, errorText);
        }
    }
    catch (Exception ex)
    {
        string exceptionData =
        $"Exception occured while getiing question by id. "  
        $"\nException Data: Message- {ex.Message}; "  
        $"InnerException- {ex.InnerException}; StackTrace- {ex.StackTrace}";

        string errorText = string.Format(stringLocalizer[Constants.ErrorCodeString],
            (int)ErroCodes.InternalServerError,
            stringLocalizer[Helper.ToEnumString(ErroCodes.InternalServerError)]);

        return StatusCode(500, errorText);
    }
}

this is how I do the service instantiation.

    public QuestionsService(IUnitOfWork unitOfWork)
    {
       _unitOfWork = unitOfWork;
    }

Get question by id function

public async Task<Questions> GetQuestionById(string id)
{
    var question = await _unitOfWork.QuestionsRepository.FindByIdAsync(id);
    return question;
}

Can someone help me to write this unit test correctly and solve this issue. I tried a lot to find out a way to do this but I could not able to do it. Thank you

CodePudding user response:

Don't - just because something can be mocked, doesn't mean it should.

Instead, you can a docker image to run Mongo, drop and create a DB per test class init, drop collections per test init.

Testing a DAL (data-access-layer) without the DB is a waste of time and won't help you actually find bugs.

When you unit test your other components, mock the entire DAL to return the objects you expect.

And do not skip writing a test for your entire service that includes an empty/pre-populated-via-test-config-data DB.

Also, the fields in your DbContext should be private, not public and while passing a constant instance of a DbContext to MongoDB is OK, because MongoDB context is stateless (no transactions and connections are pooled), in general passing a DbContext via constructor is wrong (because relational DBs have transactions and the connections should not be kept open), instead pass a Func<DbContext> (e.g. public MyClass(Func<DbContext> contextConstructor)) which returns the constructor and have DbContext implement IDisposable. This way the client class can do using (context = contextCreator()) { ... }.

CodePudding user response:

You should create a mock of the interface IQuestionsBankDBContext not the class QuestionsBankDBContext. Apart from that your test is not a unit test, it's more of an integration test because you are creating an instance of a controller and some services, and you mock only the database layer of your application. In unit tests you should test only a single layer. If you write unit tests for a controller then you should mock the services and their behavior (the direct dependencies of the controller).

Looking at your controller method, first unit test should check that when you pass not null and not empty question id then your method returns OkResult. For this test the mock of questionsService could return a question object.

Next test should check the opposite case, when it gets empty string it should return a response with code 404. (In my opinion it should be 400 but we are talking about unit tests)

Last test should check the exception handling. For example you pass a valid questionId in the test, but the mock of questionService throws an exception. In this test you can assert that the response has status code 500.

If you want to test many cases with one test method (a Theory in XUnit) then all these cases should have the same result, or you should provide an expected result with each case.

  • Related