2

Before you start:

  1. We use PostgreSQL with Postgis extension (Can't use inMemoryDB option, because of Postgis' geometry that we are using)
  2. We do not use the repository pattern
  3. We use DbContext to access the DbModels and use Linq expressions on them
  4. We use Database First principle

This sort of question has been asked before in varying degreesLike here, where they talk about it, however we do NOT implement the repository pattern but I feel it has not been answered in a concise way and so I ask it again. References or general guide is welcomed, so thank you in advance.

This is what I have so far. Controller:

public class StartTripController : Controller
{
    private readonly DbContext _dbContext;

    public StartTripController(DbContext DbContext) => _DbContext = DbContext;

    [Route("connect")]
    [HttpGet]
    public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
    {

        if (ModelState.ErrorCount > 0)
            return StatusCode(400);

        var userToCheck = await _DbContext.User
                                            .Select(i => new UserDto { UserId = i.Id, PhoneId = i.PhoneId, AppInfoDto = new AppInfoDto { IsAppInDebug = false } })
                                            .SingleOrDefaultAsync(u => u.PhoneId == startTripDto.UserDto.PhoneId);   //checks if User is in DB, returns Null if not

        if (userToCheck == null) //user does not exist
        {
          //Make new User entity and save the changes to DB async

            UserDto newUserToReturn = new UserDto { UserId = user.Id, AppInfoDto = new AppInfoDto { IsAppInDebug = user.DebugMode } };

            return GenerateResponseWithStatus200(messageDto, newUserToReturn);
        }

        //user exists
        return GenerateResponseWithStatus200(messageDto, userToCheck);
    }

My Test looks like this:

 public class StartTripControllerTest : ControllerTest<StartTripController>
{
    private DbContext _mockDbContext;

    protected override StartTripController GetController()
    {

        var mockDbContext = new Mock<DbContext>();

        var userData = new List<User>
        {
            new User{PhoneId = "Phone1", Id = 1, ReportProviderId = 1, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null},
            new User{PhoneId = "Phone2", Id = 2, ReportProviderId = 2, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null}
        };

        var mockData = userData.AsQueryable().BuildMock(); //BuildMock is from https://github.com/romantitov/MockQueryable
        mockDbContext.Setup(x => ???what do I write here??).Returns(mockData.Object);

        return new StartTripController(mockDbContext.Object);
    }



    [Fact]
    public async System.Threading.Tasks.Task StartTrip_ReturnUser_JsonAsync()
    {
        // Arrange
        StartTripController startTripController = GetController();

        MessageDto messageDto = new MessageDto ();
        StartTripDto startTripDto = new StartTripDto();
         //code omitted for readiblity
        var result = await startTripController.StartTrip(messageDto, startTripDto);
    }
}

Things I figured out:

  1. You have to Mock Interfaces in order for async methods to work As per Microsoft's post

Things I am stuck on:

  1. How do I mock the DbContext or I guess the Model in this case, so that I can use LINQ expressions, like I do I my regular code?
thefolenangel
  • 972
  • 9
  • 29
  • Not sure, but is it possible to install Postgis on Sqlite and test with Sqlite in memory? – Fabio Mar 27 '18 at 09:31
  • 3
    Anyway - mocking DbContext is complex and will create fragile tests, where you will be forced to change tests after every change in your query, even when behaviour of query remain same. – Fabio Mar 27 '18 at 09:32
  • 3
    Your method doing many things: 1. Validate ModelState, 2. Load user, 3. Validate user, 4. Create user, 5. Generate response message - where user loading and creation could be extracted to dedicated class(don't call it Repository, to keep your colleagues happy :)). Are you allowed to introduce new classes? – Fabio Mar 27 '18 at 09:40
  • I agree with Fabio that you should separate concerns, and the fragility of mocking the DbContext. If you don't want to do that, just write the test as an integration test fetching from a real database. The cost won't be that much higher but the value of that test will be unbeatable. – Richardissimo Mar 30 '18 at 22:32

1 Answers1

2

So I would like to share my answer to the problem. Which is mostly what @Fabio suggested mixed with the first reference topic i posted in my original question.

I moved my DB interactions to a "UserManager" or service if you prefer. Now my Controller looks like this:

  public class StartTripController : Controller
{

    private readonly IUserManager _userManager;

    public StartTripController( IUserManager userManager)
    {
        _userManager = userManager;
    }

    [Route("connect")]
    [HttpGet]
    public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
    {
        messageDto.Message = Any.Pack(startTripDto);

        if (ModelState.ErrorCount > 0)
            return StatusCode(400);

        var userToCheck = await _userManager.FindUser(startTripDto.UserDto);

        if (userToCheck == null) //user does not exist
        {
            var newUser = await _userManager.AddUser(startTripDto.UserDto);
            return GenerateResponseWithStatus200(messageDto, newUser);
        }

        //user exists
        await _userManager.StartTripExistingUser(userToCheck); 
        return GenerateResponseWithStatus200(messageDto, userToCheck);
    }
}

This automatically changed my test, since I do not mock the database, that simplifies my problem drastically.

My test looks like this: public class StartTripControllerTest : ControllerTest {

    protected override StartTripController GetController()
    {

        var mockUserManager = new Mock<IUserManager>();

        AppInfoDto appInfoDto = new AppInfoDto {IsAppInDebug = true};
        UserDto userDto = new UserDto {UserId = 1818, PhoneId = "Phone1", AppInfoDto = appInfoDto};

        mockUserManager.Setup(p => p.FindUser(It.IsAny<UserDto>())).Returns(Task.FromResult(userDto));
        return new StartTripController(mockUserManager.Object);
    }

    [Fact]
    [Trait("Unit", "Controller")]
    public void StartTrip_ReturnUser_BadRequestAsync()
    {
        // Arrange
        StartTripController startTripController = GetController();
        MessageDto messageDto = new MessageDto { ApiVersion = "1.3" };
        AppInfoDto appInfoDto = new AppInfoDto { IsAppInDebug = true };
        UserDto userDto = new UserDto { PhoneId = "Phone1", AppInfoDto = appInfoDto };
        StartTripDto startTripDto = new StartTripDto { UserDto = userDto };

        startTripController.ModelState.AddModelError(ModelBinderError.MissingUserId.errorKey, ModelBinderError.MissingUserId.errorValue);
        var result = startTripController.StartTrip(messageDto, startTripDto).Result as StatusCodeResult;
        Assert.True(result.StatusCode == 400);
    }
}

}

The above example shows the Controller initialization with "GetController" method and also shows how you can use the ModelState.

This however DOES NOT SOLVE the underlying problem, just moves it to a different part of system. When you have to test the UserManager, then you still have a problem with mocking the database.

For testing this part of the system, you do need a Interaction test. For SQLServer you can use InMemoryDatabase, however since I am using Postgresql I need to use a TestDatabase.

To sum it up My UserManager test looks something like this:

public class UserManagerIntegrationTests
{
 private readonly TestServer _server;
 private readonly HttpClient _client;

 public UserManagerIntegrationTests()
 {
  // Arrange
    _server = new TestServer(new WebHostBuilder()
                         .UseStartup<StartupWithTestDatabase>());//Startup file contains the TestDatabase connection string
   _client = _server.CreateClient();
 }
 // ... 
}

P.S. The thing I am looking at right now is Fluent Assertions which basically substituted the normal Assert.True() with a changed method.

P.P.S Just for future reference, the most recent tutorial I found on Unit testing for ASP.NET Core 2 is this one it also showcases an example where you do a integration test with the controller and the service that engages with your Database.

thefolenangel
  • 972
  • 9
  • 29