如何在没有实现的情况下对一个接口方法进行单元测试(C#)

7

我是一个新手,对单元测试和stackoverflow都不熟悉。

我需要测试以下接口中的RefreshAmount

public interface IAccountService
{
    double GetAccountAmount(int accountId);
}

这里有一个依赖于该接口的类:

public class AccountObj
{
    private readonly int _Id;
    private readonly IService _service;
    public AccountObj(int Id, IService service)
    {
        _Id = Id;
        _service = service;
    }
    public double Amount { get; private set; }
    public void RefreshAmount()
    {
        Amount = _service.GetAmount(_Id);
    }
}

我应该如何对 RefreshAmount 的行为进行单元测试?

RefreshAmount 调用了 IService.GetAmount,后者可能会调用后端办公室的接口,但我没有它的实现。欢迎提供任何建议。(我已经了解了 moq 和依赖注入,但我对单元测试还是很新手)


测试接口本身感觉毫无意义,因为没有实现可以测试。
  1. 编写实现并使用单元测试进行测试
  2. 正如你所提到的,你可以使用Moq来提供接口的测试实现。
- J. Tuc
@J.Tuc 我认为他不想测试界面,而是想测试 AccountObj 如何使用该界面。 - René Vogt
你的 AccountObj 看起来像一个实体。你不应该将依赖项注入到实体的构造函数中。 - Steven
5个回答

8
使用Moq,这里是一个带有注释的最小示例测试
[TestClass]
public class AccountObjUnitTests {
    [TestMethod]
    public void AccountObj_Given_Id_RefreshAmount_Should_Return_Expected_Amount() {

        //Arrange
        //creating expected values for test
        var expectedId = 1;
        var expectedAmount = 100D;
        //mock implementation of service using Moq
        var serviceMock = new Mock<IService>();
        //Setup expected behavior
        serviceMock
            .Setup(m => m.GetAmount(expectedId))//the expected method called with provided Id
            .Returns(expectedAmount)//If called as expected what result to return
            .Verifiable();//expected service behavior can be verified

        //the system under test
        var sut = new AccountObj(expectedId, serviceMock.Object);

        //Act
        //exercise method under test
        sut.RefreshAmount();


        //Assert

        //verify that expectations have been met
        serviceMock.Verify(); //verify that mocked service behaved as expected
        Assert.AreEqual(expectedAmount, sut.Amount);
    }

    //Samples class and interface to explain example
    public class AccountObj {
        private readonly int _Id;
        private readonly IService _service;
        public AccountObj(int Id, IService service) {
            _Id = Id;
            _service = service;
        }
        public double Amount { get; private set; }
        public void RefreshAmount() {
            Amount = _service.GetAmount(_Id);
        }
    }

    public interface IService {
        double GetAmount(int accountId);
    }
}

以下是同一测试的更简化版本:
[TestMethod]
public void AccountInfo_RefreshAmount_Given_Id_Should_Return_Expected_Amount() {
    //Arrange
    //creating expected values for test
    var expectedId = 1;
    var expectedAmount = 100D;
    //mock implementation of service using Moq with expected behavior
    var serviceMock = Mock.Of<IService>(m => m.GetAmount(expectedId) == expectedAmount);
    //the system under test
    var sut = new AccountObj(expectedId, serviceMock);

    //Act
    sut.RefreshAmount();//exercise method under test

    //Assert
    Assert.AreEqual(expectedAmount, sut.Amount);//verify that expectations have been met
}

Noksi,这个例子帮助我注入了多个接口。它是一个完美的工作示例。你认为Moq的这份文档https://github.com/Moq/moq4/wiki/Quickstart足够吗?还是你有其他推荐的资源? - Elim Garak
1
@Patrick 我通常建议参考相关文档以更好地掌握如何使用Moq。此外,stackoverflow上也有很多示例。 - Nkosi

2
如果你的接口叫做IAccountService,而你的服务实现的却是IService,会出现代码中一些打字错误。如果你的类名为AccountObj,但构造函数却叫AccountInfo,也会出现同样的问题。
因此,我们可以假设你的服务应该实现IAccountService,并且你的类应该命名为AccountInfo。
在编写测试AccountInfo.RefreshAmount的单元测试时,你需要了解该函数的要求。你必须确切地知道这个函数应该完成什么任务。
从代码中可以看出RefreshAmount的要求如下:
无论实现IAccountService的对象和使用哪个Id来构造AccountInfo类的对象,调用RefreshAmount后,属性Amount的值应该与IAccountService.GetAccountAmount(Id)返回的值相同。
更正式地说:
对于任何Id 对于实现IAccountService的任何类 使用Id和IAccountService构造的对象,在调用RefreshAmount()方法后,Amount属性的值应该等于IAccountService.GetAccountAmount(Id)返回的值。
由于要求它能适用于所有的Id和所有的AccountServices,因此你不能使用所有的这些进行测试。在编写单元测试时,你将不得不思考可能发生的错误。
在你的例子中,似乎没有明显的错误,但是对于未来版本,你可以考虑到函数可能会使用不正确的Id调用服务、调用错误的服务、忘记将返回值保存在Amount属性中或者Amount属性没有返回正确的值。
你类的要求指定它应该适用于每个Id和每个IAcocuntService。因此,你可以自由地提供任何你想要测试的Id和AccountService来构造你的测试对象,这些Id和AccountService能够检测到未来可能发生的四个错误中的任何一个。
幸运的是,一个简单的AccountInfo类就足以满足需求。
class AccountService : IAccountService
{
    public double GetAccountAmount(int accountId)
    {
        return 137 * accountId - 472;
    }
}

以下单元测试测试了提供的IAccountService实现返回的金额是否与多个Id对应的金额相同。
void TestAccountObj_RefreshAmount() { const int Id = 438; IAccountService accountService = new AccountService(); var testObject = new AccountInfo(Id, accountService);

   testObject.RefreshAmount();
   double amount = testObject.Amount;
   double expectedAmount = accountService.GetAccountAmount(Id);
   Assert.AreEqual(expectedAmount, amount);

这个测试将会检测所有可能的四种错误。唯一无法发现的错误是,如果调用了错误的服务并且该服务返回与奇怪的计算数值完全相同的结果,则该测试不会发现此错误。这就是我在服务中放置如此奇怪的计算的原因,任何错误的调用服务返回相同错误的概率非常小。尤其是如果您使用不同的TestObject和不同的Ids进行测试。


1

我强烈建议您在尝试实现某个接口时使用Moq。它将允许您“伪造”任何传递给您要测试的类的接口的实现。这样,您将只测试一个类的行为。从长远来看,这将使单元测试变得更加容易。

创建一个测试类似乎是一个简单的选择,但是,如果您要处理大量场景,那么该测试类将不得不增长并变得更加复杂,以满足所有场景,并且由于其复杂性,您最终也必须对其进行测试。

尝试学习如何使用Moq,这将在长期内产生回报。您将能够验证传递给模拟对象的数据,控制模拟行为返回的内容,测试模拟方法是否已被调用以及它们被调用的次数。Moq非常有用。

请查看https://github.com/Moq/moq4/wiki/Quickstart,它很好地解释了如何为初学者使用Moq。


1

这种类型的单元测试与您的类的良好组合有关。在这种情况下,您可以将“服务”传递给您的类,这样就可以为您完成工作。

您需要做的是创建“testServiceClass”,它只用于测试,不会执行其他任何操作。

注意:下面的代码缺少所有“测试”属性,以简化代码

namespace MyTests
{
    public class AccountObjTest
    {
        public void Test1()
        {
            int testVal = 10;

            AccountObj obj = new AccountObj(testVal, new AccountObjTestService());
            obj.RefreshAmount();        
            Assert.AreEquals(obj.Amount, testVal);
        }
    }

    internal class AccountObjTestService : IAccountService
    {
        public override double GetAmount(int accountId)
        {
            return accountId;
        }
    }
}

对你来说,重要的是检查类本身(单元测试),而不是跨多个类的整个实现(集成测试)。


没错。如果没有注入实现,你就无法测试接口。你需要一些形式的实现,或者模拟接口和方法。 - Sietse

0

我不熟悉Moq框架。我们使用MSTest和Microsoft Fakes,它们可以为您提供存根。

然而,一种天真的直接方法可能是在测试类中实现接口。

public class MyTestImplementation : IAccountService
{
    public bool HasBeenCalled { get; private set; }
    public int ProvidedId { get; private set; }
    public double Amoung { get; set; }

    public double GetAccountAmount(int accountId)
    {
        HasBeenCalled = true;
        ProvidedId = accountId;
    }
}

还有你的测试方法:

[TestMethod] // or whatever attribute your test framework uses
public void TestInterface()
{
    const double EXPECTEDAMOUNT = 341;
    const int EXPECTEDID = 42;
    MyTestImplementation testImpl = new MyTestImplementation();
    testImpl.Amount = EXPECTEDAMOUNT;

    var sut = new AccountObj(EXPECTEDID, testImpl);

    sut.RefreshAmount();

    // use your assertion methods here
    Assert.IsTrue(testImpl.HasBeenCalled);
    Assert.AreEqual(EXPECTEDID, testImpl.ProvidedID);
    Assert.AreEqual(EXPECTEDAMOUNT, sut.Amount);

}

如果您只想检查生成的Amount是否正确,可以省略其他属性。通常情况下,您不需要检查如何RefreshAmount()执行其应有的功能,而只需检查生成的Amount是否正确。


谢谢您的快速回答。所以如果我从您的代码中理解正确,测试是为了检查方法是否已被有效调用,以及提供的 Id 是否正确。对吗?这不是为了检查返回值是否正确。我理解得对吗? - Axel Betton
@AxelBetton 哦,对了,我加了那个验证。有时候我还是会困惑到底应该测试什么。 - René Vogt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接