昨天提到虚设常式与模拟物件的差异,两者之间之差在验证的时候如果是用该假物件验证,则为模拟物件;反之,则为虚设常式。此外,每一次的测试都应该只有一个关注点(换言之,应只有一个模拟物件),若一个测试含有多个验证(多个模拟物件),则会引发一些疑虑,举个简单的例子如下:
[Test]
public void CheckSumResult()
{
// Arrange
var sum0 = 0;
var sum1 = 0;
var sum2 = 0;
// Act
sum0 = 1001 + 1 + 2;
sum1 = 1 + 1001 + 2;
sum2 = 1 + 2 + 1001;
// Assert
Assert.AreEqual(1004, sum0);
Assert.AreEqual(1004, sum1);
Assert.AreEqual(1004, sum2);
}
OK,写到这边可以看出来我们在测试程序码 CheckSumResult 做了三次的总和,顺序互相调换,理论上总和应该都是1004;但假设今天这个互相调换的程序码没写好,变成以下的情况:
[Test]
public void CheckSumResult()
{
// Arrange
var sum0 = 0;
var sum1 = 0;
var sum2 = 0;
// Act
sum0 = 1001 + 1 + 2;
sum1 = 1 + 1 + 2;
sum2 = 1 + 2 + 1001;
// Assert
Assert.AreEqual(1004, sum0);
Assert.AreEqual(1004, sum1);
Assert.AreEqual(1004, sum2);
}
在 sum0 到 sum1 的时候,1001 与 1 应该要互相调换,但不知为何 1001 没有成功覆写第二个位置,导致 sum1 的值不符合预期,就会出错;但是,第一与三个总和应该还是要出现正确,却无法从这个测试得知。其原因在於 NUnit 的机制是在验证发生失败时,会抛出一个 AssertException 例外,NUnit 测试执行器会拦截这个例外,认为目前这个测试方法失败了,就不会继续执行下面的程序码。同理,假设有人把昨天两个的测试写在一起,改写成以下的例子:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_Fail()
{
// Arrange
StubEmailSerivce stubEmailService = new StubEmailSerivce();
FakeLogSerivce mockLogService = new FakeLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(stubEmailService, mockLogService);
// Act
var result = EmailWithLogService.SendFunction("[email protected]", "Test Demo");
// Assert
Assert.AreEqual("Fail", result);
Assert.AreEqual("[email protected] is not send yet!", mockLogService.logMessage);
}
}
public class StubEmailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Fail";
}
}
public class FakeLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
这段测试码出现了两个问题点,第一个是出现多个验证,这样的坏处是若改动程序码,如下:
public class StubEmailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
// Fail 改成 Success
// return "Fail";
return "Success";
}
}
第一眼看到这段改动,很难在第一时间看出第二个验证是否有出错,出错的问题点在哪。此外,第二个问题点是有假物件同时担任虚设常式及模拟物件。因有多个验证,在第一个验证的时候,他是扮演虚设常式;但在第二个验证的时候,又担任了模拟物件,角色呈现暧昧不清的情况,会造成程序码阅读上的困难,形成维护上的成本。
在单元测试的艺术中,过度指定是指
对一个测试单元该如何完成内部行为进行了假设,而不是只检查最终行为的正确性。
好,我相信看到这会觉得好抽象XDDD,先列出单元测试的艺术中提出的几种情况,再举个简单的例子。
那我们以「测试在需要使用虚设常式物件时,使用模拟物件」的情境并搭配昨天的状况来撰写,如下:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_CatchSendResult_Success()
{
// Arrange
MockEmailSuccessSerivce mockEmailService = new MockEmailSuccessSerivce();
StubLogSerivce stubLogService = new StubLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(mockEmailService, stubLogService);
// Act
EmailWithLogService.SendFunction("[email protected]", "Test Demo");
// Assert
Assert.AreEqual("Success", mockEmailService.SendResult);
}
}
public class MockEmailSuccessSerivce : IEmailService
{
public string SendResult
public string SendEmail(mailAddress, mailMessage)
{
SendResult = "Success";
return "Success";
}
}
public class StubLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
昨天提到当我们 SendEmail 的时候,我们去验证方法提供的回传值;然而,今天我们却以模拟物件的写法去验证是不是有做 SendEmail 这个动作,当之後若规格发生改变,改变了回传值,我们也需相对应改模拟物件的方法,这样使得程序码维护变困难却没有任何测试效益。
好,那接下来又要提更抽象的东西 XD,假设我们今天新增了一个虚设常式,然後这个虚设常式又可以再新增虚设常式,甚至可以新增要验证的模拟物件,形成一个假物件链(这难道是传说中的假物件俄罗斯娃娃套餐!?XDD)。
举个例子:
public class EmailWithLogServiceFactory()
{
public class StubEmailSuccessSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Success";
}
}
public class StubEmailFailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Fail";
}
}
public class StubLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
public class MockLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
}
可以看出来,我们把 Day-13 所用到的模拟物件都汇整在 EmailWithLogServiceFactory 里面。实务上,在工厂方法(Factory Method Pattern)的设计框架中,要撰写测试的话就很容易以这种形式撰写。因此,会随着不同的设计方式而决定假物件的设计模式,进而衍生不同的假物件链。
到这边算是把假物件做个简单的概述,撰写单元测试的核心很大一部分就是看假物件怎麽设计,而决定了後续假物件的难易程度。相信如果这几天都有在看的人会发现一件事情,我们花很大的篇幅,在撰写假物件的程序码;其实这会衍生很多问题点(撷取自单元测试的艺术):
因此,接下来明天终於要介绍另一个单元测试很大的核心概念——隔离框架(isolation framework),教你如何产制动态虚设常式物件(dynamic stub)和动态模拟物件(dynamic mock)。
<<: [Day6] Git版本控制 - 基本操作篇 (MacOS)
Tip 2:随机梯度下降法(Stochastic Gradient Descent) 提升训练速度 ...
资料型态 值 意义 0 低电位(逻辑0) 1 高电位(逻辑1) Z 高阻抗(High Impende...
今天我来讲如何使用osm在kubernetes建立VNF。如果有想要在详细的了解OSM的话可以於他们...
任何一项产品和工程的完成,除非是企业够大,能够独立用母子公司运作完成,否则以一般规模的中小企业体,产...
D7: if判断式 if的基本样子是: if(判断式){ 如果条件成立时要做甚麽 } else { ...