所以我一直在一个组织工作,给开发人员施加相当大的压力来编写和维护单元测试。虽然它不是我过去做过很多的事情,但我喜欢这个想法,并且相信任何认真的项目都应该有一定程度的单元测试,特别是对于那些适合这种测试的自我容器类库
然而,我还发现曾经非常简单,可读的代码被制成了一个怪异的工厂和接口。在最简单的情况下,服务包装器:
无单元测试
class ShazamService
{
private string url;
public ShazamService(string url) { this.url = url; }
string IdentifySong(byte [] mp3Data)
{
return HttpHelper.Upload(url, mp3Data).Response;
}
}
class Program
{
public static int Main(string [] args)
{
var svc = new ShazamService("http://www.shazam.com");
Console.Writeline(svc.IdentifySong(args[0].ToByteArray());
}
}
单元可测试版
public interface IShazamService
{
public string IdentifySong(byte [] mp3Data);
}
public class ShazamClassFactory
{
private string url;
public ShazamClassFactory(string url) { this.url = url; }
public IShazamService GetInstance(bool test)
{
if (test)
{
return new ShazamServiceTest(this.url);
}
else
{
return new ShazamService(this.url);
}
}
class ShazamService
{
private string url;
public ShazamService(string url) { this.url = url; }
string IdentifySong(byte [] mp3Data)
{
return HttpHelper.Upload(url, mp3Data).Response;
}
}
class Program
{
public static int Main(string [] args)
{
var factory = new ShazamClassFactory("http://www.shazam.com");
var svc = factory.GetInstance(false);
Console.Writeline(svc.IdentifySong(args[0].ToByteArray());
}
}
代码不仅在第二个代码中明显更长,而且(对我来说)它不太清楚 - 从Main我甚至不知道CreateInstance的返回值的类型,如果我需要看一下实现细节,所以我甚至不能轻易地通过逻辑F12。此外,该服务的1个文件现在变为4(工厂,接口,2个实现),带有标题,文档等。最后,如果我决定要将构造函数从string url
更改为{{1我现在需要检查,更新和签入4个单独的文件,更新每个文件的构造函数,数据库,文档等。
这种促进单位测试的方法是否正常?是否有较少的侵入性选择?而且,它值得吗?在我看来,通过使代码复杂化,您可以增加开发时间,并使错误更容易潜入,所有这些都是使用虚假对象进行单元测试,只会对您正在使用的代码进行排序测试。
答案 0 :(得分:8)
代码不清楚,因为写得很糟糕。
通过在setter或构造函数中注入所需的类来完成依赖注入,而不是通过硬编码不同的选项并使用GetInstance(bool)
方法来获取测试操作。
相反它看起来应该更像这样:
public class ShazamClassFactory
{
private string url;
private IShazamService _shazamService;
public ShazamClassFactory(string url) { this.url = url; }
public void SetShazamService(IShazamService service) {
_shazamService = service;
}
public string GetSong(){
return _shazamService.IdentifySong(url.ToByteArray());
}
}
现在您可以像这样使用它:
var factory = new ShazamClassFactory("http://www.shazam.com");
factory.SetShazamService(new ShazamTestService());
var song = factory.GetSong();
答案 1 :(得分:2)
我在这里看到的问题是,你不能立即清楚你要测试的是什么。
如果您正在编写使用 a ShazamService
的代码,那么您可以传递具体实现或测试实现,具体取决于它是否是单元测试。
如果在创建对象时需要控制,则应使用工厂,并且在传递依赖项时不应该(imo)成为默认模式。
对于您的实例,可以选择更好的选项。
服务接口
public interface IShazamService
{
string IdentifySong(byte [] mp3Data);
}
实际实时界面
public class LiveShazamService : IShazamService
{
private readonly string _url;
public LiveShazamService(string url)
{
_url = url;
}
public string IdentifySong(byte [] mp3Data)
{
return HttpHelper.Upload(url, mp3Data).Response;
}
}
测试界面(可能存在于您的测试项目中)
public class MockShazamService : IShazamService
{
private readonly string _testData;
public LiveShazamService(string testData)
{
_testData = testData;
}
public string IdentifySong(byte [] mp3Data)
{
return _testData;
}
}
测试代码
[Test]
public void ShouldParseTitleOfSong()
{
// arrange
var shazamService = new MockShazamService(
"<html><title>Bon Jovi - Shock to the Heart</title></html>");
var parser = new ShazamMp3Parser(shazamService);
// act
// this is just dummy input,
// we're not testing input in this specific test
var result = parser.Parse(new byte[0]);
// assert
Assert.AreEqual("Bon Jovi - Shock to the Heart", result.Title);
}
生产代码
public class ShazamMp3Parser
{
private readonly IShazamService _shazamService;
public ShazamMp3Parser(IShazamService shazamService)
{
_shazamService = shazamService;
}
public ShazamParserResult Parse(byte[] mp3Data)
{
var rawText = _shazamService.IdentifySong(mp3Data);
// bla bla bla (up to the viewer to implement properly)
var title = rawText.SubString(24, 50);
return new ShazamParserResult { Title = title };
}
}
生产代码的使用
public static int Main(string [] args)
{
var service = new LiveShazamService("http://www.shazam.com");
var parser = new ShazamMp3Parser(service);
var mp3Data = args[0].ToByteArray();
Console.Writeline(parser.Parse(mp3Data).Title);
}
在这个例子中,我将展示如何测试依赖于IShazamService
(ShazamMp3Parser
)的代码,这使您可以单元测试标题的解析,而无需进行互联网连接和提取实时数据。模拟服务允许您模拟数据并单元测试解析代码的工作方式。
我没有实现工厂,因为我觉得在这种情况下不需要它,但如果你想控制服务实例化的时间,你可以编写一个工厂接口,然后是两个实现,一个构建实时服务和构建测试的服务。
如果你以后变得勇敢,或者你厌倦了在各地编写模拟类,你可以使用模拟框架(如moq)来让你的单元测试写得更快。
[Test]
public void ShouldParseTitleOfSong()
{
// arrange
var mockShazamService = new Mock<IShazamService>();
mockShazamService.Setup(x => x.IdentifySong(It.IsAny<byte[]>()))
.Returns("<html><title>Bon Jovi - Shock to the Heart</title></html>");
var parser = new ShazamMp3Parser(mockShazamService.Object);
// act
var result = parser.Parse(new byte[0]);
// assert
Assert.AreEqual("Bon Jovi - Shock to the Heart", result.Title);
}
答案 2 :(得分:1)
我认为您正在寻找的是abstract factory。通过提供抽象工厂本身的接口,您可以传递创建测试对象的工厂或创建真实对象的工厂,而不必检测代码。