Unit testing 不依赖于实现细节的测试

Unit testing 不依赖于实现细节的测试,unit-testing,testing,tdd,bdd,acceptance-testing,Unit Testing,Testing,Tdd,Bdd,Acceptance Testing,想象一下以下人为的例子: public class LoginController { private readonly IValidate _validator; private readonly IAuthenticate _authenticator; public LoginController(IValidate validator, IAuthenticate authenticator) { _validator = validator;

想象一下以下人为的例子:

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);
}
通常,我会编写如下测试:

[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();
    }
}
[TestFixture]
公共类无效请求
{
私人登录请求_invalidRequest;
专用IValidate _验证器;
私有HttpStatusCode\u响应;
void givenrequest()
{
_invalidRequest=新登录请求();
}
void和giventhatRequestsinValid(){
_validator=替换为();
_validator.IsValid(_invalidRequest)。返回值(false);
}
当尝试登录()时无效
{
_响应=新登录控制器(_验证程序,null)
.Login(_invalidRequest);
}
void然后应使用badRequest()响应
{
AreEqual(HttpStatusCode.BadRequest,_response);
}
[测试]
public void Execute()
{
这个.BDDfy();
}
}
公共类登录成功
{
私人登录请求;
专用IValidate _验证器;
私有IAAuthenticate\u认证;
私有HttpStatusCode\u响应;
void givenrequest()
{
_请求=新登录请求();
}
void和giventhatRequestsValid(){
_validator=替换为();
_IsValid(_请求)。返回(true);
}
无效,但提供的原始凭证不存在(){
_authenticate=替换.For();
_authenticate.IsAuthenticated(
_请求。电子邮件,
_请求密码
)。报税表(虚假);
}   
当尝试登录()时无效
{
_响应=新登录控制器(\u验证程序,\u身份验证)
.Login(_请求);
}
void则应响应unauthorized()
{
Assert.AreEqual(HttpStatusCode.Unauthorized,_响应);
}
[测试]
public void Execute()
{
这个.BDDfy();
}
}
然而,在观看了下面的视频并进行了更多的阅读之后,我开始认为我的测试与代码的实现联系太紧密了。例如,我试图在第一个实例中测试的行为是,如果我们尝试使用无效请求登录,我们将使用错误请求的http状态代码进行响应。问题是,我正在通过存根
IValidate
依赖项来测试这一点。如果实现者决定
IValidate
抽象不再有用,并决定在
Login
方法中内联验证请求,那么系统的行为没有改变,但是我的测试现在中断了

但是,唯一的替代方案是集成测试,在该测试中,我启动web服务器,点击登录端点并在响应上断言。问题在于,这是脆弱而复杂的,因为我们最终需要在第三方凭证存储中有一个有效的用户来测试用户登录成功的场景


因此,我的问题是,我的理解是否不正确,或者在针对实现的测试和全面集成测试之间是否有一个中间地带?

我遵循BDD方法,首先通过验收测试(即集成测试)和单元测试(必要时)来测试系统,以驱动细节。验收测试独立于实现,因为它们仅通过用户界面与系统交互。单元测试必然依赖于实现,因为每个测试只测试一个类(您的示例实际上是控制器的单元测试),但您只需要在验收测试没有涵盖所有行为时编写它们,所以至少在某些时候您可以避免与实现紧密耦合的测试

我特别发现,在功能完善的web应用程序中,验收测试通常几乎完全覆盖控制器,几乎不需要对控制器进行单元测试。控制器委托的模型和其他类需要大量的单元测试,但这些类往往具有更有意义的行为,并且单元测试更高效


这就剩下了如何处理外部凭证存储。如果无法针对实际存储编写验收测试(您在生产实例中没有存储的测试实例或测试帐户),那么请实际操作并加以验证。确保您正在集成测试尽可能多的代码,方法是将实际与存储联系的代码放在其自己的类中,不受业务逻辑的限制,并且只保留该类。您可以为store adapter类编写一两个单元测试,测试与store的连接是否正常工作。

针对实现进行测试不是一个好主意。使用您的实现向您建议好的测试,这些测试可能会暴露bug。在适当的TDD中,您从一个失败的测试用例开始,这样您就知道您有一个测试用例,它可能会因为至少一个错误(不完整)的实现而失败

事实上,单元测试和集成测试之间并没有明确的分离。几乎所有有用的类都使用其他类,即使是语言库提供的基本类(如字符串)。最好把测试看作是连续的,在完美的单元测试和完美的集成测试之间。您应该努力使代码中的一些测试接近完美的单元测试结束,但如果它们不完美,不要心烦意乱

如果您编写的类a与另一个类B协作,那么使用类B的真实对象而不是模拟对象来测试a并不总是错误的。如果您确实使用了mock对象,我建议您使用一个mock来复制被模拟类的所有相关行为,并施加真实类的所有约束(前提条件检查)。仅验证使用特定参数调用特定方法的mock通常不太有用。模拟测试,使用模拟对象的公共接口验证其最终状态是否如预期的更好;然后,测试将不依赖于