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

时间:2016-01-16 04:33:02

标签: c# validation unit-testing code-contracts

我有这样的界面:

[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;
}

我有一个相应的合约类:

[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接口的具体实现我的要求。

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

更新

因此,虽然模拟接口并测试验证逻辑似乎不起作用,但我的具体类(不是从摘要继承)在测试中正确验证输入。因此,我可能不会在嘲笑中支持它,但我不知道为什么。

3 个答案:

答案 0 :(得分:1)

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

因为你试图模仿界面,所以模拟工具对合同一无所知。所有模拟工具都会查看接口的定义并创建代理对象。 代理是一个替身,一个双重,它根本就没有行为!现在,对于像Moq这样的库,您可以使用Returns(It.Is.Any())等方法使这些代理具有行为。但同样,此时此处将代理更多地转换为存根。而且,更重要的是,由于一个原因,这不适用于模拟库:代理在运行期间在运行期间动态创建代理。因此,ccrewrite不会“重写”代理

那么您如何测试您为合同指定了合适的条件?

例如,您应该创建一个名为 MyProjectName.Tests.Stubs 的新库。然后,您应该在此项目中为您的接口创建一个实际的存根对象实例。它不必详细说明。足以允许您在单元测试中调用方法来测试合同是否按预期工作。哦,还有一个更重要的工作:启用 对这个新创建的存根项目执行运行时协议检查以进行Debug构建。否则,您创建的从接口继承的存根将不会使用合同进行检测。

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

// 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。还有一个网站:http://xunitpatterns.com/

答案 1 :(得分:0)

据我了解您的示例代码,您的系统测试(SUT)是ContractStockDataProvider类,但您正在针对模拟IStockDataProvider运行测试。事实上,你的SUT中的代码不会受到影响。

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

为了您的答案,我将您的代码简化为此(主要是因为我没有在此计算机上设置或安装Code Contracts):

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

我们需要解决的一件事是ContractStockDataProviderabstract。解决这个问题的方法是在测试类中使用虚拟实现:

[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 来提供抽象类的实现:

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

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

就我个人而言,我喜欢保持我的存根和我的嘲distinct明显,并且感觉这些混淆了水,并且对于发生的事情并不完全清楚。但是,如果你反对第一个例子中的class StubContractStockDataProvider : ContractStockDataProvider片段,那么这是另一种获得测试的方式。

答案 2 :(得分:0)

无论出于何种原因,我都无法获得一个模拟接口来抛出预期的异常;话虽这么说,我能够通过界面的每个实现来测试异常。这有点令人沮丧,似乎面对DRY原则,但我能够对这些合同进行单元测试。