My understanding of mocking vs not mocking is that mocking too much creates brittle tests that need to be changed all the time, while on the other hand less mocking better verifies that the system works as it's supposed to all the way through.
I can see the value of not mocking too much in an untyped language since, but there are some parts that I have trouble understanding when it comes to a layered architecture in a typed language. I hope it's possible to understand what I'm asking here, but if not don't hesitate to comment!
Say I have the following controller and accompanying service classes in an MVC application. (This example is in C# but I hope it's possible to follow even without knowledge.)
public class ApplicationController
{
private IApplicationService _service;
public ApplicationController(IApplicationService service)
{
_service = service;
}
[HttpPost]
public ActionResult LoginUser(string id)
{
var result = _service.LoginUser(id);
if (!result) return Unauthorized();
return Ok();
}
}
public class ApplicationService
{
private DbContext _dbContext;
private HttpContextAccessor _httpContextAccessor;
public void LoginUSer(DbContext dbContext, HttpContextAccessor httpContextAccessor)
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
}
public bool LoginUser(string id)
{
var user = _dbContext.Users.Get(id);
if (user == null) return false;
if (!user.Active) return false;
_httpContextAccessor.HttpContext.User.Identity.Id = user.Id;
return true;
}
}
When testing the ApplicationController I can think of two simple tests: - If result is false, return Unauthorized - If result is true, return Ok
Not mocking anything - tight coupling?
Since my approach in this example is to not use any mocks, I need to somehow simulate ApplicationService returning false. One way is to seed the database with a user that has the property Active = false
.
[Fact]
public void LoginUser_UserNotActive_ReturnsUnauthorized()
{
var sut = new ApplicationController();
_dbContext.Add(new User { Id = "123", Active = false });
var result = sut.LoginUser("123");
Assert.IsType<UnauthorizedResult>(result);
}
This is where it gets tricky for my understanding. In order to create this test, my ApplicationController test has to "know" in which instances the ApplicationService
will return false
and set up the environment properly, which creates coupling, right? Let me illustrate what I mean.
Now consider me writing tests for the ApplicationService
. A simple test would be the following.
public void LoginUser_UserNotActive_ReturnsFalse()
{
var sut = new ApplicationService();
_dbContext.Add(new User { Id = "123", Active = false });
var result = sut.LoginUser("123");
Assert.False(result);
}
With this coupling which (from my understanding) stems from not writing black-box tests, if a later requirement says that you can login users which are active, I have to change both tests!
Furthermore if I write an ApplicationController
test to test UserNotActive
, I have to test for UserDoesNotExist
too, right? But at that point I'm duplicating tests, just in different layers.
Wouldn't it be better with mocking..?
If on the other hand I wrote the controller test using mocks, it would maybe look like this.
public void LoginUser_UserNotActive_ReturnsUnauthorized()
{
var serviceMock = Substitute.For<ApplicationService>();
serviceMock.LoginUser(Arg.Any<string>()).Returns(false);
var sut = new ApplicationController(serviceMock);
var result = sut.LoginUser("someId");
Assert.IsType<UnauthorizedResult>(result);
}
With this test, if I change the requirements to be able to login with an inactive user, I would not need to change the ApplicationController
test since I'm treating ApplicationService
like a black box; I'm just testing return values!
So this is where my understanding breaks down. How would I go about black-box testing without mocks? It seems like I have to write white box tests for everything, which seems to create brittle tests that have to change as soon as as requirements change down the chain. At that point, why am I even separating functionality into classes and layers?
The example in this question is very simple but I hope it gets the point across; one could imagine an example with more layers where the test complexity would only grow.