C# 单元测试是否不必要地混淆了代码,或者有更好的方法吗?
因此,我一直在一个组织中工作,该组织对开发人员编写和维护单元测试施加了相当大的压力。虽然我在过去并没有做过很多,但我喜欢这个想法,并且相信任何严肃的项目都应该有一定程度的单元测试,特别是对于能够进行这种测试的自容器类库 然而,我还发现,曾经非常简单、可读的代码变成了工厂和接口的怪物。在最简单的情况下,服务包装器: 无单元测试 单元可测试版本 不仅第二个版本的代码要长得多,而且(对我来说)不太清楚——从Main来看,如果我需要查看实现细节,我甚至不知道CreateInstance返回值的类型,所以我甚至无法轻松地通过逻辑进行F12。此外,服务的1个文件现在变成4个(工厂、接口、2个实现),带有标题、文档等。最后,如果我决定将构造函数从C# 单元测试是否不必要地混淆了代码,或者有更好的方法吗?,c#,unit-testing,C#,Unit Testing,因此,我一直在一个组织中工作,该组织对开发人员编写和维护单元测试施加了相当大的压力。虽然我在过去并没有做过很多,但我喜欢这个想法,并且相信任何严肃的项目都应该有一定程度的单元测试,特别是对于能够进行这种测试的自容器类库 然而,我还发现,曾经非常简单、可读的代码变成了工厂和接口的怪物。在最简单的情况下,服务包装器: 无单元测试 单元可测试版本 不仅第二个版本的代码要长得多,而且(对我来说)不太清楚——从Main来看,如果我需要查看实现细节,我甚至不知道CreateInstance返回值的类型,所以
string url
更改为string url,songgree
,我现在需要签出、更新和签入4个单独的文件,更新每种类型的构造函数、数据成员、文档等
这种促进单元测试的方法是规范吗?是否有较少干扰的选项?那么,这值得吗?在我看来,通过使代码复杂化,您增加了开发时间,并使错误更容易潜入,所有这些都是为了使用伪对象进行单元测试,而伪对象只会在某种程度上测试您正在使用的代码。代码不清楚,因为它写得不好 依赖项注入是通过在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();
现在您可以这样使用它:
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();
我想你要找的是一份工作。通过提供一个抽象工厂本身的接口,您可以传递创建测试对象的工厂或创建真实对象的工厂,而不必插入代码。我在这里看到的问题是,无法立即清楚您要测试什么 如果您正在编写使用
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 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;
}
}
测试接口(可能存在于您的测试项目中)
测试代码
[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
)的代码,这使您可以单元测试标题解析,而无需进行internet连接和提取实时数据。模拟服务允许您模拟数据和单元测试解析代码的工作方式
我没有实现工厂,因为我觉得在这个场景中没有必要,但是如果您想控制服务的实例化时间,您可以编写一个工厂接口,然后是两个实现,一个构建实时服务,一个构建测试服务
如果你以后变得勇敢,或者你厌倦了到处编写模拟类,你可以使用模拟框架(比如)来加快单元测试的编写速度
[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);
}
[测试]
公共空间应为歌曲()
{
//安排
var mockShazamService=new Mock();
mockShazamService.Setup(x=>x.IdentifySong(It.IsAny())
.返回(“Bon Jovi-心惊肉跳”);
var parser=new-ShazamMp3Parser(mockShazamService.Object);
//表演
var result=parser.Parse(新字节[0]);
//断言
Assert.AreEqual(“Bon Jovi-心惊肉跳”,result.Title);
}
这与单元测试无关,不知道你是从哪里来的这个工厂,如果你在工厂里的话,这很可怕。这只是一个糟糕的设计,它与单元测试毫无关系。不管单元测试如何,编码到接口而不是具体的类会使代码更加灵活。为了避免带有bool参数的丑陋工厂,您可能希望查看IoC,以便在配置文件中设置所有这些,而不是使代码混乱。简短回答:单元测试不应该让代码变得更糟糕,它应该让代码更糟糕。只要弄清楚你的测试需要模拟什么,并让它有可能通过。在本例中,使用frex代替硬编码HttpHelper.Upload
,您可以传入Upload
委托。如果您不想一直这样做,那么您可以有两个构造函数,一个(用于测试)接受委托,另一个用于硬编码委托的链式构造函数。我建议发布一个不同的问题,“我如何使此可测试”?编写测试是从需求开始,以实现结束的过程的一部分。您是否先编写一个测试,然后对测试内容进行足够的实现以使测试通过?单元测试是代码的使用者。如果你的代码对消费者不友好,那就是一个问题,或者至少是一种气味。这是一个很好的答案,但我不明白为什么你仍然有一个工厂类(或者至少命名为工厂类),或者你为什么选择setter而不是constructorinjection@BenAaronson:我刚刚接管了ShazamclassFactory的名称;它不是一个实际的工厂(没有可用的工厂方法)。我使用setter而不是构造函数,因为构造函数已经有了一个参数,我想保持它的清晰(当然两者都很好,尽管构造函数也是我的首选)。