单元测试是否会不必要地混淆代码,还是有更好的方法?

时间:2014-06-05 23:18:39

标签: c# unit-testing

所以我一直在一个组织工作,给开发人员施加相当大的压力来编写和维护单元测试。虽然它不是我过去做过很多的事情,但我喜欢这个想法,并且相信任何认真的项目都应该有一定程度的单元测试,特别是对于那些适合这种测试的自我容器类库

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

无单元测试

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个单独的文件,更新每个文件的构造函数,数据库,文档等。

这种促进单位测试的方法是否正常?是否有较少的侵入性选择?而且,它值得吗?在我看来,通过使代码复杂化,您可以增加开发时间,并使错误更容易潜入,所有这些都是使用虚假对象进行单元测试,只会对您正在使用的代码进行排序测试。

3 个答案:

答案 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);
}

在这个例子中,我将展示如何测试依赖于IShazamServiceShazamMp3Parser)的代码,这使您可以单元测试标题的解析,而无需进行互联网连接和提取实时数据。模拟服务允许您模拟数据并单元测试解析代码的工作方式。

我没有实现工厂,因为我觉得在这种情况下不需要它,但如果你想控制服务实例化的时间,你可以编写一个工厂接口,然后是两个实现,一个构建实时服务和构建测试的服务。

如果你以后变得勇敢,或者你厌倦了在各地编写模拟类,你可以使用模拟框架(如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。通过提供抽象工厂本身的接口,您可以传递创建测试对象的工厂或创建真实对象的工厂,而不必检测代码。