C# 如何在没有实现的情况下对接口方法进行单元测试
C# How to unit test an interface method without implementation
我是单元测试和 Whosebug 的新手。
我要在下面的界面中测试RefreshAmount
:
public interface IAccountService
{
double GetAccountAmount(int accountId);
}
这是一个依赖于此接口的 class:
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 框架。我们使用 MSTest 和 Microsoft Fakes 可以为您提供存根。
然而,一个天真的直接方法可能是在测试中实现接口 class
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
是正确的。
此类事情的单元测试与您 class 的良好组合有关。在这种情况下是。您可以将 "the service" 传递给您的 class,这应该会为您完成工作。
您需要做的是制作 "testServiceClass",它仅用于测试,不会做任何其他事情。
注意:下面的代码缺少所有 "tests" 属性以简要说明代码
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;
}
}
}
检查 class 本身对您来说很重要 (UNIT-test),而不是跨 (INTEGRATION-test) 的多个 class 的整个实施。
使用最小起订量,这是一个带注释的最小示例测试
[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
}
我强烈建议您使用最小起订量来实现您想要实现的目标。它将允许您 "Fake" 传递给要测试的 class 的任何接口的实现。这样您一次只能测试一个 class 的行为。这将使单元测试在长期 运行.
中变得容易得多
创建测试 class 似乎是一个简单的选择,但是,如果您要处理大量场景,则该测试 class 将不得不增长并变得更加复杂迎合所有场景,在某些时候你也必须测试它,因为它很复杂。
尝试学习如何使用 moq,它会在长期得到回报 运行。您将能够验证传递给模拟对象的数据,控制模拟行为返回的内容,测试模拟方法是否已被调用以及它们被调用了多少次。最小起订量非常有用。
看看 https://github.com/Moq/moq4/wiki/Quickstart 它向初学者解释了如何很好地使用 Moq
我假设您的代码中是否有一些输入错误,因为您的接口被调用 IAccountService
而您的服务实现 IService
。你的 class 被称为 AccountObj
而你的构造函数被称为 AccountInfo
.
所以让我们假设您的服务预计将实施 IAccountService
并且您的 class 预计将命名为 AccountInfo
.
编写单元测试来测试AccountInfo.RefreshAmount
时,您必须了解此功能的要求;你必须确切地知道这个函数应该做什么。
看代码好像RefreshAmount
的要求是:
Whatever object that implements IAccountService
and whatever Id are used to construct an object of class AccountInfo
, the post condition of calling RefreshAmount
is that property Amount
returns the same value as IAccountService.GetAccountAmount(Id)
would have returned.
或更正式:
- 对于任何 ID
- 对于实现
IAccountService
的任何 class
- 使用 Id 和 IAccountService 创建的对象应该在调用 RefreshAmount() 后,return 属性 Amount 的值,等于由 [=61] 编辑的值 return =](Id).
因为要求说它应该适用于所有 ID 和所有 AccountServices,所以您不能用所有这些来测试您的 class。在这种情况下,在编写单元测试时,您必须考虑可能发生的错误。
在你的例子中看起来没有可能的错误,但对于未来的版本,你可能会想到该函数会使用不正确的 Id 调用服务,或者调用不正确的服务,或者忘记保存 return 值在 属性 金额中,或者 属性 金额不是 return 正确的值。
您的 class 的要求指定它应该适用于每个 Id 和每个 IAcocuntService。因此,您可以自由地向要测试的测试对象的构造函数提供任何 Id 和 AccountService,这可能会检测到四个未来错误中的任何一个。
幸运的是,一个简单的 AccountInfo class 就足够了
class AccountService : IAccountService
{
public double GetAccountAmount(int accountId)
{
return 137 * accountId - 472;
}
}
以下单元测试测试 Amount 是 return由提供的 IAccountService 实现为几个 ID 编辑的金额
无效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);
}
此测试将测试所有提到的四个可能的错误。它找不到的唯一错误是,如果这个不正确的服务 return 完全相同的奇怪计算数字,它会调用不正确的服务。这就是为什么我在服务中放置如此奇怪的计算,任何错误调用的服务都不太可能 return 同样的错误。特别是如果您要使用具有各种 ID
的各种 TestObject 进行测试
我是单元测试和 Whosebug 的新手。
我要在下面的界面中测试RefreshAmount
:
public interface IAccountService
{
double GetAccountAmount(int accountId);
}
这是一个依赖于此接口的 class:
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 框架。我们使用 MSTest 和 Microsoft Fakes 可以为您提供存根。
然而,一个天真的直接方法可能是在测试中实现接口 class
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
是正确的。
此类事情的单元测试与您 class 的良好组合有关。在这种情况下是。您可以将 "the service" 传递给您的 class,这应该会为您完成工作。
您需要做的是制作 "testServiceClass",它仅用于测试,不会做任何其他事情。
注意:下面的代码缺少所有 "tests" 属性以简要说明代码
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;
}
}
}
检查 class 本身对您来说很重要 (UNIT-test),而不是跨 (INTEGRATION-test) 的多个 class 的整个实施。
使用最小起订量,这是一个带注释的最小示例测试
[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
}
我强烈建议您使用最小起订量来实现您想要实现的目标。它将允许您 "Fake" 传递给要测试的 class 的任何接口的实现。这样您一次只能测试一个 class 的行为。这将使单元测试在长期 运行.
中变得容易得多创建测试 class 似乎是一个简单的选择,但是,如果您要处理大量场景,则该测试 class 将不得不增长并变得更加复杂迎合所有场景,在某些时候你也必须测试它,因为它很复杂。
尝试学习如何使用 moq,它会在长期得到回报 运行。您将能够验证传递给模拟对象的数据,控制模拟行为返回的内容,测试模拟方法是否已被调用以及它们被调用了多少次。最小起订量非常有用。
看看 https://github.com/Moq/moq4/wiki/Quickstart 它向初学者解释了如何很好地使用 Moq
我假设您的代码中是否有一些输入错误,因为您的接口被调用 IAccountService
而您的服务实现 IService
。你的 class 被称为 AccountObj
而你的构造函数被称为 AccountInfo
.
所以让我们假设您的服务预计将实施 IAccountService
并且您的 class 预计将命名为 AccountInfo
.
编写单元测试来测试AccountInfo.RefreshAmount
时,您必须了解此功能的要求;你必须确切地知道这个函数应该做什么。
看代码好像RefreshAmount
的要求是:
Whatever object that implements
IAccountService
and whatever Id are used to construct an object of classAccountInfo
, the post condition of callingRefreshAmount
is that propertyAmount
returns the same value asIAccountService.GetAccountAmount(Id)
would have returned.
或更正式:
- 对于任何 ID
- 对于实现
IAccountService
的任何 class
- 使用 Id 和 IAccountService 创建的对象应该在调用 RefreshAmount() 后,return 属性 Amount 的值,等于由 [=61] 编辑的值 return =](Id).
因为要求说它应该适用于所有 ID 和所有 AccountServices,所以您不能用所有这些来测试您的 class。在这种情况下,在编写单元测试时,您必须考虑可能发生的错误。
在你的例子中看起来没有可能的错误,但对于未来的版本,你可能会想到该函数会使用不正确的 Id 调用服务,或者调用不正确的服务,或者忘记保存 return 值在 属性 金额中,或者 属性 金额不是 return 正确的值。
您的 class 的要求指定它应该适用于每个 Id 和每个 IAcocuntService。因此,您可以自由地向要测试的测试对象的构造函数提供任何 Id 和 AccountService,这可能会检测到四个未来错误中的任何一个。
幸运的是,一个简单的 AccountInfo class 就足够了
class AccountService : IAccountService
{
public double GetAccountAmount(int accountId)
{
return 137 * accountId - 472;
}
}
以下单元测试测试 Amount 是 return由提供的 IAccountService 实现为几个 ID 编辑的金额
无效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);
}
此测试将测试所有提到的四个可能的错误。它找不到的唯一错误是,如果这个不正确的服务 return 完全相同的奇怪计算数字,它会调用不正确的服务。这就是为什么我在服务中放置如此奇怪的计算,任何错误调用的服务都不太可能 return 同样的错误。特别是如果您要使用具有各种 ID
的各种 TestObject 进行测试