当多个单元测试复制同一个文件时,运行 所有单元测试都失败

When multiple unit tests copy the same file, running all unit tests fail

描述

我正在为一种方法编写单元测试,该方法将文件从源复制到目标。基本上它包括这段代码:

public void MyMethod() 
{
    // ...
    File.Copy(source, destination, true);
    // ...
}

在我的单元测试项目中,我有一个测试文件:(test.png),它位于我的单元测试项目的Resources文件夹中。我已经将 Copy to Output 属性 设置为 Always

我有 3 个单元测试正在测试此方法。

当他们点击复制文件的代码行时:source = "Resources\test.png".

问题

当我运行单独进行单元测试时,它们都通过了,一切都很好。 但是,当我 运行 Visual Studio 中的所有测试时,我得到这个 运行 时间错误并且单元测试失败:

System.IO.DirectoryNotFoundException

Could not find a part of the path 'Resources\test.png'.

我的想法...(更新)

问题

我该如何解决这个问题?

有什么配置设置可以解决这个问题吗?

当多个单元测试访问同一个文件时,我如何 运行 Visual Studio(和 Team City)中的所有单元测试?

我推测您的问题是其中一种测试方法更改了目录,给出了明确的 "Directory Not Found" 异常。文件锁定或任何并发问题不太可能导致所描述的行为。

访问同一个文件应该没有问题。确保您没有用于删除文件的 cleanUp Fixture(TestSuite 级别)。因为从异常来看,文件似乎在 运行 测试后被删除。

并发读取操作也很好并且完全合法。如果您的单元测试正在覆盖文件,那么这是一个问题。

您可以按照 MSDN: Executing Unit Tests in parallel on a multi-CPU/core machine 中的说明,将 parallelTestCount 设置为 1,尝试排除多线程问题。如果现在测试通过,您就缩小了问题范围。

但是,如果当你 运行 他们在一个组中时你的测试仍然失败 - 我认为这是更有可能的情况 - 那么我的建议是检查这些测试共享的任何状态.您描述的模式(即隔离通过;不隔离时失败)是(错误地)共享状态的测试通常表现出的一种症状,并且这些测试正在修改该状态,导致一个或多个测试失败。

如果进行单元测试,您实际上不应该测试 File.Copy(或任何 File class 方法)是否有效,因为您没有编写该代码。相反,您应该测试您的代码是否与文件类型正确交互(即它是否在您调用 "Copy" 时传递了正确的源文件名、目标文件名和覆盖值)。首先为 File class 创建一个接口,并为它创建一个实现该接口的包装器;

public interface IFileWrapper
{
    void Copy(string sourceFileName,string destFileName,bool overwrite);
    //Other required file system methods and properties here...
}

public class FileWrapper : IFileWrapper
{
    public void Copy(string sourceFileName, string destFileName, bool overwrite)
    {
        File.Copy(sourceFileName, destFileName, overwrite);
    }
}

然后您应该使您正在测试的 class 包含一个 IFileWrapper 参数 (dependency injection)。在您的单元测试中,您可以使用 Moq 等模拟框架,或者您可以编写自己的模拟;

public class MockFileWrapper : IFileWrapper
{
    public string SoureFileName { get; set; }
    public string DestFileName { get; set; }
    public bool Overwrite { get; set; }
    public void Copy(string sourceFileName, string destFileName, bool overwrite)
    {
        SoureFileName = sourceFileName;
        DestFileName = destFileName;
        Overwrite = overwrite;
    }
}

在实际实现中,将 FileWrapper 作为 IFileWrapper 参数传入,但在您的单元测试中,将 MockFileWrapper 传入。通过在单元测试中检查 mockFileWrapper 的属性,您现在可以确定是否 class 调用了 Copy 以及它是如何调用的。由于您不再在单元测试之间共享真实文件,因此您将避免测试共享状态或可能锁定文件的机会。

发生的事情是,由于我使用的是测试文件的相对路径,出于某种原因,当 运行 单元测试批量时,测试运行器工作目录与 运行 单独测试时不同, 因此找不到目录。

所以我使用这个函数来构建测试文件的绝对路径:

    private string GetFilePath([CallerFilePath] string path = "")
    {
        return path;
    }

然后:

    string projectDir = Path.GetDirectoryName(GetFilePath());
    string testFile = Path.Combine(projectDir, @"Resources\test.png";

正如您提到的 ,测试框架并不总是 运行 将工作目录设置为构建输出文件夹进行测试。

要指示测试框架将构建工件或构建输出中的其他文件放入测试目录,您需要使用 DeploymentItemAttribute。对于你的情况,你会做类似的事情:

const string destination = "Destination.txt";
const string source = "MyData.txt";

[DeploymentItem(source)]
[TestMethod]
public void MyMethod() 
{
    // …
    File.Copy(source, destination, true);
    // …
}

[TestCleanup]
public void Cleanup()
{
    // Clean up the destination so that subsequent tests using
    // the same deploy don’t collide.
    File.Delete(destination);
}

还要确保您的文件标有内容构建操作和始终复制。否则,它们不会在构建输出目录中,也无法复制到测试目录中。