C# 单元测试是否不必要地混淆了代码,或者有更好的方法吗?

C# 单元测试是否不必要地混淆了代码,或者有更好的方法吗?,c#,unit-testing,C#,Unit Testing,因此,我一直在一个组织中工作,该组织对开发人员编写和维护单元测试施加了相当大的压力。虽然我在过去并没有做过很多,但我喜欢这个想法,并且相信任何严肃的项目都应该有一定程度的单元测试,特别是对于能够进行这种测试的自容器类库 然而,我还发现,曾经非常简单、可读的代码变成了工厂和接口的怪物。在最简单的情况下,服务包装器: 无单元测试 单元可测试版本 不仅第二个版本的代码要长得多,而且(对我来说)不太清楚——从Main来看,如果我需要查看实现细节,我甚至不知道CreateInstance返回值的类型,所以

因此,我一直在一个组织中工作,该组织对开发人员编写和维护单元测试施加了相当大的压力。虽然我在过去并没有做过很多,但我喜欢这个想法,并且相信任何严肃的项目都应该有一定程度的单元测试,特别是对于能够进行这种测试的自容器类库

然而,我还发现,曾经非常简单、可读的代码变成了工厂和接口的怪物。在最简单的情况下,服务包装器:

无单元测试

单元可测试版本

不仅第二个版本的代码要长得多,而且(对我来说)不太清楚——从Main来看,如果我需要查看实现细节,我甚至不知道CreateInstance返回值的类型,所以我甚至无法轻松地通过逻辑进行F12。此外,服务的1个文件现在变成4个(工厂、接口、2个实现),带有标题、文档等。最后,如果我决定将构造函数从
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而不是构造函数,因为构造函数已经有了一个参数,我想保持它的清晰(当然两者都很好,尽管构造函数也是我的首选)。