【问题标题】:TDD with C#, issue with writing tests with mock使用 C# 进行 TDD,使用模拟编写测试时出现问题
【发布时间】:2020-03-17 08:27:37
【问题描述】:

我正在 TDD 中迈出第一步(我正在学习 Bender 和 McWherter 的“Professional Test-Driven Development with C#”)。

我正在尝试使用 TDD 编写我的第一个应用程序:我假设有一个 DataService 类来管理持久性。我写了两个通过的测试,但我认为我没有明白这一点。

这是我的第一个测试,我假设我可以实例化一个事务,无论它在 DataService 中意味着什么

public void Begin_a_transaction_should_returns_true_when_is_all_ok()
{
    Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
    DataService sut = new DataService(dataLayer.Object);

    bool expected = true;
    dataLayer.Setup(dl => dl.BeginTransaction()).Returns(expected);

    bool actual = sut.BeginTransaction();

    Assert.AreEqual(expected, actual);
}

现在根据 TDD,我编写了类,这没有问题

public class DataService
{
    private IDataLayer _dataLayer;

    public DataService(IDataLayer dataLayer)
    {
        this._dataLayer = dataLayer;
    }

    public bool BeginTransaction()
    {
        return _dataLayer.BeginTransaction();
    }
}

现在我想写第二个测试:如果事务已经存在,BeginTransaction 应该失败,我要求 IDataLayer 这样做

[Test]
public void Begin_a_transaction_should_throws_exception_if_transaction_already_exists()
{
    Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
    DataService sut = new DataService(dataLayer.Object);

    dataLayer.Setup(dl => dl.BeginTransaction()).Throws(new Exception("Transaction already exists"));

    Assert.Throws<Exception>(() => sut.BeginTransaction());
}

现在重点是:测试通过而无需编写任何代码行,因为我已模拟 BeginTransaction 以引发异常。 可以,因为我将在 IDataLayer 测试的实现中对其进行测试,但如果我全部模拟,DataService 测试是否有用?

【问题讨论】:

  • DataService 不任何事情,它只是数据层的外观/代理,因此单元测试的效用可能非常低。您的两个测试实际上是相同的:给定一个注入的数据层,调用数据服务方法调用数据层上的一个方法并返回结果。

标签: c# unit-testing tdd


【解决方案1】:

我会说发生这种情况是因为您正在测试一个除了包装 IDataLayer 之外没有任何行为的类的行为 - 包装类中发生的任何事情都只是简单地传递到外部。当方法返回 true 时,您所描述的情况也会发生相同的情况,尽管它不太明显。

为了使您的示例更有意义,您可以添加一些取决于 IDataLayer 的 BeginTransaction() 结果的行为,例如;

实施

public class DataService
{
    public DataService(IDataLayer dataLayer, IBookRepository bookRepository) 
    {
        _dataLayer = dataLayer;
        _bookRepository = bookRepository;
    }

    public void StoreBookInfo(string data)
    {
        if (_dataLayer.BeginTransaction())
            _bookRepository.StoreBookInfo(data);
        else
            throw new SomeException();
    }
}

测试

[Test]
public void Should_store_book_info_if_transaction_can_be_started()
{
    Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
    Mock<IBookRepository> bookRepository = new Mock<IBookRepository>();

    dataLayer.Setup(dl => dl.BeginTransaction()).Returns(true);
    bookRepository.Setup(x => x.StoreBookInfo(It.IsAny<string>()));

    DataService sut = new DataService(dataLayer.Object, bookRepository.Object);

    sut.StoreBookInfo("SomeValue");

    bookRepository.Verify(x => x.StoreBookInfo(It.IsAny<string>()));
}

[Test]
public void Should_throw_exception_if_transaction_cannot_be_started()
{
    Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
    Mock<IBookRepository> bookRepository = new Mock<IBookRepository>();

    dataLayer.Setup(dl => dl.BeginTransaction()).Returns(false);

    DataService sut = new DataService(dataLayer.Object, bookRepository.Object);

    Assert.Throws<SomeException>(() => sut.StoreBookInfo("someValue"));
}

【讨论】:

  • Tnx。 TDD 可以吗?那么,我将不得不为 IDataLayer 的实现编写测试?
  • 嗯,使用 TDD,您应该为您实现的所有内容编写测试(先测试,后实现)。我会尝试用一个更好的例子来详细说明我的答案:)
【解决方案2】:

仅仅为了测试而暴露内部结构通常不是好的做法。在您的情况下, _transaction 受到保护,以便装饰器可以对其内部状态进行更改。我最好建议模仿客户的行为。

    [Test]
    public void Begin_a_transaction_should_throws_exception_if_transaction_already_exists()
    {
        Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
        DataService sut = new DataService(dataLayer.Object);

        sut.BeginTransaction();

        Assert.Throws<Exception>(() => sut.BeginTransaction());
    }

【讨论】:

    【解决方案3】:

    嗯,我用这种方式检查了我的代码。

    public class DataService
        {
            private IDataLayer _dataLayer;
            protected object _transaction;
    
            public DataService(IDataLayer dataLayer)
            {
                this._dataLayer = dataLayer;
            }
    
            public bool BeginTransaction()
            {
                if (_transaction != null)
                    throw new Exception("Transaction already exists");
    
                _transaction = _dataLayer.BeginTransaction();
    
                return true;
            }
        }
    

    比我在测试类中使用装饰器模式

    internal class DataServiceDecorator : DataService
            {
                public DataServiceDecorator(IDataLayer dataLayer) : base(dataLayer)
                { }
    
                public void InjectTransactionObject(object transaction)
                {
                    this._transaction = transaction;
                }
            }
    

    所以我可以写这个测试

    [Test]
            public void Begin_a_transaction_should_throws_exception_if_transaction_already_exists()
            {
                Mock<IDataLayer> dataLayer = new Mock<IDataLayer>();
                DataServiceDecorator sut = new DataServiceDecorator(dataLayer.Object);
    
                sut.InjectTransactionObject(new object());
    
                Assert.Throws<Exception>(() => sut.BeginTransaction());
            }
    

    您对此有何看法?

    【讨论】:

      猜你喜欢
      • 2011-06-11
      • 1970-01-01
      • 2021-03-09
      • 1970-01-01
      • 1970-01-01
      • 2023-04-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多