Imagine the following contrived example:
public class LoginController {
private readonly IValidate _validator;
private readonly IAuthenticate _authenticator;
public LoginController(IValidate validator, IAuthenticate authenticator) {
_validator = validator;
_authenticator = authenticator;
}
public HttpStatusCode Login(LoginRequest request) {
if (!_validator.IsValid(request)) {
return HttpStatusCode.BadRequest;
}
if (!_authenticator.IsAuthenticated(request.Email, request.Password)) {
return HttpStatusCode.Unauthorized;
}
return HttpStatusCode.OK;
}
}
public class LoginRequest {
public string Email {get; set;}
public string Password {get; set;}
}
public interface IValidate {
bool IsValid(LoginRequest request);
}
public interface IAuthenticate {
bool IsAuthenticated(string email, string password);
}
Typically I would write tests like the following:
[TestFixture]
public class InvalidRequest
{
private LoginRequest _invalidRequest;
private IValidate _validator;
private HttpStatusCode _response;
void GivenARequest()
{
_invalidRequest = new LoginRequest();
}
void AndGivenThatRequestIsInvalid() {
_validator = Substitute.For<IValidate>();
_validator.IsValid(_invalidRequest).Returns(false);
}
void WhenAttemptingLogin()
{
_response = new LoginController(_validator, null)
.Login(_invalidRequest);
}
void ThenShouldRespondWithBadRequest()
{
Assert.AreEqual(HttpStatusCode.BadRequest, _response);
}
[Test]
public void Execute()
{
this.BDDfy();
}
}
public class LoginUnsuccessful
{
private LoginRequest _request;
private IValidate _validator;
private IAuthenticate _authenticate;
private HttpStatusCode _response;
void GivenARequest()
{
_request = new LoginRequest();
}
void AndGivenThatRequestIsValid() {
_validator = Substitute.For<IValidate>();
_validator.IsValid(_request).Returns(true);
}
void ButGivenTheLoginCredentialsDoNotExist() {
_authenticate = Substitute.For<IAuthenticate>();
_authenticate.IsAuthenticated(
_request.Email,
_request.Password
).Returns(false);
}
void WhenAttemptingLogin()
{
_response = new LoginController(_validator, _authenticate)
.Login(_request);
}
void ThenShouldRespondWithUnauthorized()
{
Assert.AreEqual(HttpStatusCode.Unauthorized, _response);
}
[Test]
public void Execute()
{
this.BDDfy();
}
}
However after watching the following video Ian Cooper: TDD, where did it all go wrong and doing some more reading, I'm starting to think that my tests are too closely tied to the implementation of the code. For instance, the behavior I'm trying to test in the first instance is that if we try to login with an invalid request we response with the http status code of bad request. The issue is that I'm testing this by stubbing the IValidate
dependency. If the implementer decides the IValidate
abstraction is no longer useful and decides to validate the request inline in the Login
method, then the behaviour of the system hasn't changed however my tests now break.
But then, the only other alternate is an integration test where I launch the web server and hit the login endpoint and assert on the response. The issue is that this is brittle and complicated, as we would ultimately need to have a valid user in the third party credential store for testing the user login successful scenario.
So my question is, is my understanding incorrect, or is there a middle ground between testing against the implementation and full-blown integration testing?