【问题标题】:Should unit tests tests the functionality of a method? [closed]单元测试应该测试方法的功能吗? [关闭]
【发布时间】:2019-05-13 23:38:26
【问题描述】:

我正在编写单元测试,但最让我困惑的部分是它是否应该测试功能?

例如,如果有一个方法可以做两件事

  1. 从文件夹中删除文件
  2. 返回文件夹是否为空
public bool DeleteFTPFiles(string xyz)
{
    ...

    path = GetFTPPath(xyz);
    DeleteFolderFiles(path);
    return IsFtpFolderEmpty(path);
}

DeleteFolderFiles - 根据某些逻辑删除文件。

现在,如果我必须对此方法进行单元测试 (DeleteFTPFiles)。 我是否必须通过单元测试创​​建文件夹结构并添加一些文件作为排列测试?

根据条件判断文件是否被删除?

另外,根据是否为空来测试IsFtpFolderEmpty是否返回true或false?

如果是这样,这与集成测试有何不同?

【问题讨论】:

  • 虽然基于意见,但该方法做了太多事情(违反 SRP),并且从事情的外观来看,与实现问题紧密相关,这会使孤立地测试它变得困难。您提到必须创建实际文件夹/文件的事实表明这也是一个集成测试。

标签: c# unit-testing testing automated-tests


【解决方案1】:

例如,如果有一个方法可以做两件事

您选择编写DeleteFTPFiles() 的方法是一个糟糕的选择,因为结果与名称不匹配。如果文件没有被删除,该方法可能仍然返回true?这是错误的逻辑。如果我使用该代码,我会假设结果是文件是否被删除,不是如果目录为空。

如果我要写它,那就是DeleteAllFiles(),因为它不需要知道它发生在哪里,它就是它。然后我会传入另一个具有完成工作所需方法的类。

public class MySpaceManager()
{
  private readonly IFileManager _fileManager;
  public MySpaceManager(IFileManager fileManager)
  {
    _fileManager = fileManager;
  }


  public bool TryDeleteAllFiles1(logicalDirectory)
  {
    var files = _fileManager.GetFiles(logicalDirectory);
    var result = true;
    foreach(var file in files)
      result = result && _fileManager.Delete(file);

    return result;
  }

  // or maybe

  public bool TryDeleteAllFiles2(logicalDirectory)
  {
    var files = _fileManager.GetFiles(logicalDirectory);
    foreach(var file in files)
      _fileManager.Delete(file);

    var result = _fileManager.GetFiles(logicalDirectory).Count() == 0;
    return result;
  }

}

单元测试应该测试方法的功能吗?

这是我的解释:

单元测试应该只测试它要封装的内容。这可能包括以下一项或多项(不一定是详尽的列表):

  1. 运行完成
  2. 引发异常
  3. 某种类型的逻辑(例如,AddTwoNumber() 确实做到了这种逻辑)
  4. 执行一些外部依赖
  5. 不执行某些外部依赖项

让我们来看看这个假设的课程,并分解每个测试的内容和原因:

public class MySpaceManagerTests
{
  // First simple, best good path for code
  public void TryDeleteAllFiles2_WithEmptyPath_ThrowsNoException()
  {
    /// ** ASSIGN **

    // I'm using NSubstitute here just for an example
    // could use Moq or RhinoMocks, whatever doesn't  
    // really matter in this instance
    // the important part is that we do NOT test dependencies
    // the class relies on.
    var fileManager = Substitute.For<IFileManager>();
    fileManager
      .GetFiles(Args.Any<string>())
      .Returns(new List<IFile>());

    var mySpaceManager = new MySpaceManager(fileManager);

    // ** ACT && ASSERT**

    // we know that the argument doesn't matter so we don't need it to be
    // anything at all, we just want to make sure that it runs to completion
    Asser.DoesNotThrow(() => mySpaceManager.TryDeleteAllFiles2(string.Empty);
  }

  // This looks VERY similar to the first test but
  // because the assert is different we need to write a different
  // test.  Each test should only really assert the name of the test
  // as it makes it easier to debug and fix it when it only tests
  // one thing.
  public void TryDeleteAllFiles2_WithEmptyPath_CallsFileManagerGetFiles()
  {
    /// ** ASSIGN **
    var fileManager = Substitute.For<IFileManager>();
    fileManager
      .GetFiles(Args.Any<string>())
      .Returns(new List<IFile>());

    var mySpaceManager = new MySpaceManager(fileManager);

    // ** ACT **
    mySpaceManager.TryDeleteAllFiles2(string.Empty)

    // ** ASSERT **
    Assert.DoesNotThrow(fileManager.Received().GetFiles());
  }

  public void TryDeleteAllFiles2_With0Files_DoesNotCallDeleteFile
  {
    /// ** ASSIGN **
    var fileManager = Substitute.For<IFileManager>();
    fileManager
      .GetFiles(Args.Any<string>())
      .Returns(new List<IFile> { Substitute.For<IFile>(); });

    var mySpaceManager = new MySpaceManager(fileManager);

    // ** ACT **
    mySpaceManager.TryDeleteAllFiles2(string.Empty)

    // ** ASSERT **
    Assert.DoesNotThrow(fileManager.DidNotReceive().GetFiles());
  }

  public void TryDeleteAllFiles2_With1File_CallsFileManagerDeleteFile
  {
    // etc
  }

  public void TryDeleteAllFiles2_With1FileDeleted_ReturnsTrue()
  {
    /// ** ASSIGN **
    var fileManager = Substitute.For<IFileManager>();
    fileManager
      .GetFiles(Args.Any<string>())
      .Returns(new List<IFile> { Substitute.For<IFile>(); }, 
        new list<IFile>());

    var mySpaceManager = new MySpaceManager(fileManager);

    // ** ACT **
    var actual = mySpaceManager.TryDeleteAllFiles2(string.Empty)

    // ** ASSERT **
    Assert.That(actual, Is.True);
  }

  public void TryDeleteAllFiles2_With1FileNotDeleted_ReturnsFalse()
  {
    /// ** ASSIGN **
    var fileManager = Substitute.For<IFileManager>();
    fileManager
      .GetFiles(Args.Any<string>())
      .Returns(new List<IFile> { Substitute.For<IFile>(); }, 
        new List<IFile> { Substitute.For<IFile>(); });

    var mySpaceManager = new MySpaceManager(fileManager);

    // ** ACT **
    var actual = mySpaceManager.TryDeleteAllFiles2(string.Empty)

    // ** ASSERT **
    Assert.That(actual, Is.False);
  }
}

【讨论】:

  • 我以为我回复了这篇文章。非常感谢您提供列表和示例。这是综合反应。起来!
【解决方案2】:

单元测试可以测试这段代码,但应该以另一种方式编写。

查看这段代码更有意义的是谈论集成测试而不是单元测试。

要具备编写单元测试的能力,需要将您的代码与具体实现分离。您想测试您的代码而不是 FTP 服务,是吗?

要使代码可测试,需要按照以下步骤重构代码:

引入IFileStorage-抽象

public interface IFileStorage
{
    string GetPath(string smth);
    void DeleteFolder(string name);
    bool IsFolderEmpty(string path);    
}

public sealed class FtpFileStorage : IFileStorage
{
    public string GetPath(string smth) { throw new NotImplementedException(); }
    public void DeleteFolder(string name) { throw new NotImplementedException(); }
    public bool IsFolderEmpty(string path) { throw new NotImplementedException(); }
}

代码应该依赖于抽象而不是具体的实现:

public class SmthLikeServiceOrManager
{
    private readonly IFileStorage _fileStorage;

    public SmthLikeServiceOrManager(IFileStorage fileStorage)
    {
        _fileStorage = fileStorage;
    }    

    public bool DeleteFiles(string xyz)
    {
        // ...

        var path = _fileStorage.GetPath(xyz);
        _fileStorage.DeleteFolder(path);
        return _fileStorage.IsFolderEmpty(path);
    }
}

现在您可以使用模拟库之一编写真正的单元测试,例如

StackOverflow 上的相关文章:

【讨论】:

  • 感谢@vladmir。鉴于上面的代码,当你对DeleteFTPFiles进行单元测试时,你会不会在单元测试中写一些代码在本地创建文件夹进行测试?
  • 不应该创建任何“物理”人工制品(例如文件夹或文件),而是需要使用模拟库之一来创建假存储,该存储将具有具体单元所需的强定义行为-测试。我建议看一些像Intro to Mocking with Moq 这样的快速入门文档。理想情况下是通过 Pluralsight/Youtube 或其他地方的课程,这不需要太多时间;)
  • 感谢您的解释。我们对此进行了讨论,我认为我们不应该创建任何文件夹或不应该越界。所以想在这里征求意见。这绝对有帮助。
  • @Kar - 欢迎,很高兴为您提供帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-07
  • 2011-01-29
  • 1970-01-01
  • 2012-12-13
  • 1970-01-01
  • 2016-11-16
相关资源
最近更新 更多