测试时不依赖于实现细节

kim*_*gro 8 testing tdd bdd unit-testing acceptance-testing

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

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);
}
Run Code Online (Sandbox Code Playgroud)

通常我会编写如下测试:

[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();
    }
}
Run Code Online (Sandbox Code Playgroud)

然而,在观看了以下视频后,Ian Cooper:TDD,哪里出了问题并做了一些阅读,我开始认为我的测试与代码的实现过于紧密相关.例如,我试图在第一个实例中测试的行为是,如果我们尝试使用无效请求登录,我们将使用错误请求的http状态代码进行响应.问题是我通过存根IValidate依赖来测试这个.如果实现者决定IValidate抽象不再有用并且决定在Login方法中内联验证请求,那么系统的行为没有改变,但是我的测试现在已经破坏了.

但是,唯一的另一个替代方案是集成测试,我在其中启动Web服务器并点击登录端点并对响应进行断言.问题是这很脆弱,因为我们最终需要在第三方凭证存储中拥有一个有效用户来测试用户登录成功方案.

所以我的问题是,我的理解是不正确的,还是在测试与实施和全面集成测试之间存在中间立场?

Mar*_*ann 13

与我们交易的大多数其他方面一样,涉及权衡取舍.

  • 如果您在单位级别进行测试,某些测试可能会太脆弱.
  • 如果您在行为级别进行测试,则无法涵盖所有​​情况.

很多人宣布单元测试和测试驱动开发(TDD)已经死亡,并将行为驱动开发(BDD)视为新的银弹.显然,它们都不是银子弹.

在你的问题中,你已经概述了单元测试的一种问题,所以尽管我想回到那些问题,让我们先来看看BDD.

集成测试的问题

在他的开创性演讲中,集成测试是一个骗局,JB Rainsberger解释了为什么集成测试(包括大多数BDD式测试)存在问题.你真的应该看到录音,但它的本质是整合测试涉及测试用例的组合爆炸.

考虑一下你自己的琐碎例子.该Login方法LoginController具有3的Cyclomatic Complexity,因为它有3种方法.如果只想测试行为,则需要将其与其依赖项的相应实现集成.

只要看一眼的方法签名,我们可以看到,因为两者_validator.IsValid_authenticator.IsAuthenticated返回bool,必须有至少 2种方式,通过他们每个人.

因此,利用这些乐观数字,积分这三个对象的排列数的上限是3*2*2 = 12.实际数字小于那个,因为你在某些分支机构中提前返回,但数量级是正确的.问题在于,如果例如验证器具有更高程度的复杂性,并且特别是如果它具有其自身的依赖性,则可能的组合的数量爆炸,并且快速达到五位或六位数字.

你无法编写所有这些测试用例.

单元测试的问题

编写单元测试时,可以减少组合数量.您可以它们添加到一起,以便了解您必须编写的测试用例的数量,而不必将所有可能的代码路径组合相乘.这使您可以减少测试次数,并且可以获得更好的覆盖率.事实上,您可以通过单元测试获得完美的覆盖率.

那么,问题正如您所描述的那样.从某种意义上说,您可以测试实现的感觉.它是,但它只是实施一部分,这就是重点.尽管如此,这意味着当事情发生变化时,单元测试会受到影响,集成测试应该在很小程度上受到影响.

采用仅附加策略进行测试有点帮助,但它仍然可能感觉像开销一样.

测试金字塔

所有这些都解释了为什么Mike Cohn推荐测试金字塔:

  • 大量单位单元测试,以确保您正确构建的东西.
  • 集成测试,以确保您正在构建正确的东西.