证明我的合同正在验证正确的事情

Proving my contracts are validating the right thing

我的界面是这样的:

[ContractClass(typeof(ContractStockDataProvider))]
public interface IStockDataProvider
{
    /// <summary>
    /// Collect stock data from cache/ persistence layer/ api
    /// </summary>
    /// <param name="symbol"></param>
    /// <returns></returns>
    Task<Stock> GetStockAsync(string symbol);

    /// <summary>
    /// Reset the stock history values for the specified date
    /// </summary>
    /// <param name="date"></param>
    /// <returns></returns>
    Task UpdateStockValuesAsync(DateTime date);

    /// <summary>
    /// Updates the stock prices with the latest values in the StockHistories table.
    /// </summary>
    /// <returns></returns>
    Task UpdateStockPricesAsync();

    /// <summary>
    /// Determines the last population date from the StockHistories table, and 
    /// updates the table with everything available after that.
    /// </summary>
    /// <returns></returns>
    Task BringStockHistoryCurrentAsync();

    event Action<StockEventArgs> OnFeedComplete;
    event Action<StockEventArgs> OnFeedError;
}

我有一个相应的合同 class 像这样:

[ContractClassFor(typeof (IStockDataProvider))]
public abstract class ContractStockDataProvider : IStockDataProvider
{
    public event Action<StockEventArgs> OnFeedComplete;
    public event Action<StockEventArgs> OnFeedError;

    public Task BringStockHistoryCurrentAsync()
    {
        return default(Task);
    }

    public Task<Stock> GetStockAsync(string symbol)
    {
        Contract.Requires<ArgumentException>(!string.IsNullOrWhiteSpace(symbol), "symbol required.");
        Contract.Requires<ArgumentException>(symbol.Equals(symbol.ToUpperInvariant(), StringComparison.InvariantCulture),
            "symbol must be in uppercase.");
        return default(Task<Stock>);
    }

    public Task UpdateStockPricesAsync()
    {
        return default(Task);
    }

    public Task UpdateStockValuesAsync(DateTime date)
    {
        Contract.Requires<ArgumentOutOfRangeException>(date <= DateTime.Today, "date cannot be in the future.");
        return default(Task);
    }
}

我做了一个这样的单元测试:

[TestClass]
public class StockDataProviderTests
{
    private Mock<IStockDataProvider> _stockDataProvider;

    [TestInitialize]
    public void Initialize()
    {
        _stockDataProvider = new Mock<IStockDataProvider>();
    }

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public async Task GetStockAsyncSymbolEmptyThrowsArgumentException() 
    {
        //arrange
        var provider = _stockDataProvider.Object;

        //act
        await provider.GetStockAsync(string.Empty);

        //assert
        Assert.Fail("Should have thrown ArgumentException");
    }
}

根据我的阅读,这应该足以通过单元测试,但在执行时,单元测试因未抛出异常而失败。

我不是要测试合同功能,但我有兴趣测试验证逻辑以确保满足我对 IStockDataProvider 接口的具体实现的要求。

我做错了吗?我如何使用我的单元测试来验证我是否正确指定了我的输入?

更新

因此,虽然模拟接口和测试验证逻辑似乎不起作用,但我的具体 class(不是从抽象继承)在测试中正确验证了输入。所以它可能只是不支持模拟,虽然我不太清楚为什么。

据我了解您的示例代码,您的被测系统 (SUT) 是 ContractStockDataProvider class 但您 运行 是针对 IStockDataProvider 的 Mock 进行的测试.按照目前的情况,您的 SUT 中的代码不会受到影响。

你应该只需要模拟 ContractStockDataProvider 的依赖关系而不是它的接口。

为了您的回答,我将您的代码简化为(主要是因为我没有在这台机器上设置或安装 Code Contracts):

public abstract class ContractStockDataProvider
{
    public void GetStockAsync(string symbol)
    {
        if (string.IsNullOrWhiteSpace(symbol))
        {
            throw new ArgumentException();
        }
    }
 }

我们需要解决的一件事是 ContractStockDataProviderabstract。解决这个问题的方法是在测试 class:

中有一个虚拟实现
[TestClass]
public class StockDataProviderTests
{
    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void GetStockAsyncSymbolEmptyThrowsArgumentException()
    {
        //arrange
        var sut = new StubContractStockDataProvider();

        //act
        sut.GetStockAsync(string.Empty);
    }

    public class StubContractStockDataProvider : ContractStockDataProvider
    {
    }
}

我们不需要 Assert.Fail 因为如果满足 ExpectedException 它会自动失败。

第二种方法是使用 Moq 来实现抽象 class:

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void GetStockAsyncSymbolEmptyThrowsArgumentException1()
    {
        //arrange
        var sut = new Mock<ContractStockDataProvider>();

        //act
        sut.Object.GetStockAsync(string.Empty);
    }

就我个人而言,我喜欢让我的存根和我的模拟截然不同,并且觉得这有点混淆了水并且不完全清楚发生了什么。但是,如果您反对第一个示例中的 class StubContractStockDataProvider : ContractStockDataProvider 片段,那么这是进行测试的另一种方式。

出于某种原因,我无法获得模拟接口来抛出预期的异常;也就是说,我能够测试接口的每个实现的异常。这有点令人沮丧,它似乎与 DRY 原则背道而驰,但我能够对这些合同进行单元测试。

您的模拟没有抛出异常的原因很简单。接口不能有方法。因此,您不能直接在接口上指定合同。但是,你已经知道了这一点。这就是为什么你为你的界面创建了一个契约 class(顺便说一下,它应该是一个 private abstract class)。

因为您正试图模拟界面,所以模拟工具对合同一无所知。所有模拟工具所做的就是查看接口的定义并创建一个 proxy 对象。一个proxy就是替身,替身,一点行为都没有!现在,有了像 Moq 这样的库,您可以使用 Returns(It.Is.Any()) 等方法使这些代理具有行为。但是,此时这又将 proxy 变成了 stub。此外,更重要的是,由于一个原因,这不适用于模拟库:proxy 是在运行时 on the fly 创建的测试。因此 代理 的 "rewriting" 没有被 ccrewrite 执行。

那么您将如何测试您是否为合同指定了正确的条件?

例如,您应该创建一个名为 MyProjectName.Tests.Stubs 的新库。然后,您应该在该项目中为您的界面创建一个实际的 stub 对象实例。它不必详细说明。足以让您调用单元测试中的方法来测试合约是否按预期工作。哦,还有一件更重要的事情:Enable Perform Runtime Contract Checking 在这个新创建的调试构建存根项目上。否则,您创建的继承自您的接口的 存根 将不会使用契约进行检测。

在您的单元测试项目中引用这个新的 MyProjectName.Tests.Stubs 程序集。使用存根来测试您的接口。这是一些代码(请注意,我正在使用您 post 中的代码——所以如果合同没有按预期工作,请不要怪我——修复你的代码;)):

// Your Main Library Project
//////////////////////////////////////////////////////////////////////

[ContractClass(typeof(ContractStockDataProvider))]
public interface IStockDataProvider
{
    /// <summary>
    /// Collect stock data from cache/ persistence layer/ api
    /// </summary>
    /// <param name="symbol"></param>
    /// <returns></returns>
    Task<Stock> GetStockAsync(string symbol);

    /// <summary>
    /// Reset the stock history values for the specified date
    /// </summary>
    /// <param name="date"></param>
    /// <returns></returns>
    Task UpdateStockValuesAsync(DateTime date);

    /// <summary>
    /// Updates the stock prices with the latest values in the StockHistories table.
    /// </summary>
    /// <returns></returns>
    Task UpdateStockPricesAsync();

    /// <summary>
    /// Determines the last population date from the StockHistories table, and 
    /// updates the table with everything available after that.
    /// </summary>
    /// <returns></returns>
    Task BringStockHistoryCurrentAsync();

    event Action<StockEventArgs> OnFeedComplete;
    event Action<StockEventArgs> OnFeedError;
}

// Contract classes should:
//    1. Be Private Abstract classes
//    2. Have method implementations that always
//       'throw new NotImplementedException()' after the contracts
//
[ContractClassFor(typeof (IStockDataProvider))]
private abstract class ContractStockDataProvider : IStockDataProvider
{
    public event Action<StockEventArgs> OnFeedComplete;
    public event Action<StockEventArgs> OnFeedError;

    public Task BringStockHistoryCurrentAsync()
    {
        // If this method doesn't mutate state in the class,
        // consider marking it with the [Pure] attribute.

        //return default(Task);
        throw new NotImplementedException();
    }

    public Task<Stock> GetStockAsync(string symbol)
    {
        Contract.Requires<ArgumentException>(
            !string.IsNullOrWhiteSpace(symbol),
            "symbol required.");
        Contract.Requires<ArgumentException>(
            symbol.Equals(symbol.ToUpperInvariant(), 
                StringComparison.InvariantCulture),
            "symbol must be in uppercase.");

        //return default(Task<Stock>);
        throw new NotImplementedException();
    }

    public Task UpdateStockPricesAsync()
    {
        // If this method doesn't mutate state within
        // the class, consider marking it [Pure].

        //return default(Task);
        throw new NotImplementedException();
    }

    public Task UpdateStockValuesAsync(DateTime date)
    {
        Contract.Requires<ArgumentOutOfRangeException>(date <= DateTime.Today, 
            "date cannot be in the future.");

        //return default(Task);
        throw new NotImplementedException();
    }
}

// YOUR NEW STUBS PROJECT
/////////////////////////////////////////////////////////////////
using YourNamespaceWithInterface;

// To make things simpler, use the same namespace as your interface,
// but put '.Stubs' on the end of it.
namespace YourNamespaceWithInterface.Stubs
{
    // Again, this is a stub--it doesn't have to do anything
    // useful. So, if you're not going to use this stub for
    // checking logic and only use it for contract condition
    // checking, it's OK to return null--as you're not actually
    // depending on the return values of methods (unless you
    // have Contract.Ensures(bool condition) on any methods--
    // in which case, it will matter).
    public class StockDataProviderStub : IStockDataProvider
    {
        public Task BringStockHistoryCurrentAsync()
        {
            return null;
        }

        public Task<Stock> GetStockAsync(string symbol)
        {
            Contract.Requires<ArgumentException>(
                !string.IsNullOrWhiteSpace(symbol),
                "symbol required.");
            Contract.Requires<ArgumentException>(
                symbol.Equals(symbol.ToUpperInvariant(), 
                    StringComparison.InvariantCulture),
                "symbol must be in uppercase.");

            return null;
        }

        public Task UpdateStockPricesAsync()
        {
            return null;
        }

        public Task UpdateStockValuesAsync(DateTime date)
        {
            Contract.Requires<ArgumentOutOfRangeException>(
                date <= DateTime.Today, 
                "date cannot be in the future.");

            return null;
        }
    }
}

// IN YOUR UNIT TEST PROJECT
//////////////////////////////////////////////////////////////////
using YourNamespaceWithInteface.Stubs

[TestClass]
public class StockDataProviderTests
{
    private IStockDataProvider _stockDataProvider;

    [TestInitialize]
    public void Initialize()
    {
        _stockDataProvider = new StockDataProviderStub();
    }

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public async Task GetStockAsyncSymbolEmptyThrowsArgumentException() 
    {
        //act
        await provider.GetStockAsync(string.Empty);

        //assert
        Assert.Fail("Should have thrown ArgumentException");
    }
}

通过创建包含接口存根实现的项目并在存根项目上启用执行运行时合同检查,您现在可以在单元测试中测试合同条件。

我还强烈建议您阅读一些有关单元测试和各种 测试替身 的角色的文章。有一次,我认为模拟、存根、假货不是一回事。好吧,是的,不是。答案有点微妙。不幸的是,像 MoQ 这样的库虽然很棒!但无济于事,因为它们往往会混淆您在使用这些库时在测试中实际使用的内容。同样,这并不是说它们没有帮助、没有用或没有用处——只是说您在使用这些库时需要准确了解您使用的是什么。我可以提出的建议是 xUnit Test Patterns. There's also a website: http://xunitpatterns.com/.