2

这是我的代码:

public void GenerateCarDetailsFile(IList<int> carIds, string location)
{

   var cars = Uow.Query<Car>().Where(x => carIds.Contains(x.Id));

   var stringWriter = new StringWriter();

   stringWriter.WriteLine("Make, Model, Year");

   foreach(var car in cars)
   {
     stringWriter.WriteLine("{0},{1},{2}", car.Make, car.Model, car.Year);
   }

   SaveToFile(stringWriter, location);
}

public void SaveToFile(StringWriter stringWriter, string location)
{
   var bytes = new System.Text.UTF8Encoding().GetBytes(stringWriter.ToString());
   var file = System.IO.File.OpenWrite(location);
   file.Write(bytes, 0, bytes.Length);
   file.Close();
}

所以我从数据库中得到了一堆汽车。将它们写入 stringWriter 然后将它们保存到文件中。

我的问题是如何对正确的信息保存到文件进行单元测试。这是不可测试的吗?它更像是一个集成测试吗?

我无法想象该怎么做,因为这两种方法都返回 void。

4

5 回答 5

2

您可以通过读取保存的文件并对其进行解析来测试它是否保存了正确的数据。如果可以正确解析数据,则应“正确保存”。如果您询问代码是否实际上将文件保存到磁盘,那是 FileStream 类的单元测试,而不是您的类。

不过,使用模拟库来测试类的行为会更容易。下面是在 C# 社区中似乎很流行的三个框架的比较;Rhino Mocks vs Moq vs NSubstitute。我还会推荐NUnit(也可以与 nuget 一起使用),因为它是一个很好的测试框架。

使用模拟框架时,您创建了一个您的类可以使用的“假”对象。这也意味着您应该使用依赖注入(即向您的类注入依赖)。您的类的依赖项似乎是一个数据库访问类和一个文件访问类。通过将这些传递给您的班级,您也遵循了单一职责原则,简单来说就是一个班级应该只知道一件事。(例如,它不应该知道如何访问数据库以及如何访问文件)

只需根据您的需要创建两个接口,IDatabaseRepository或者IFileStorage类似的东西。然后将这些实例注入你的类。当您创建单元测试时,这些很容易被模拟。例如,使用 Rhino 模拟,单元测试可以按照这个思路进行。

public interface IDatabaseProvider {
    IEnumerable<Car> GetCars();
}

public interface IFileStorage {
    string ReadText(string filepath);
    void SaveText(string filepath, string content);
}

public class MyClass {
    private readonly IDatabaseProvider dataProvider;
    private readonly IFileStorage storage;

    public MyClass(IDatabaseProvider dataProvider, IFileStorage storage) {
        this.dataProvider = dataProvider;
        this.storage = storage;
    }

    public void GenerateCarDetailsFile(IList<int> carIds, string location) {
        var cars = dataProvider.GetCars().Query<Car>().Where(x => carIds.Contains(x.Id));
        StringBuilder builder = new StringBuilder();
        builder.AppendLine("Make, Model, Year");

        foreach(var car in cars) {
            builder.WriteLine("{0},{1},{2}", car.Make, car.Model, car.Year);
        }

        storage.SaveText(location, builder.ToString());
    }
}

[Test]
public void GenerateCarDetailsSavesFile() {
    // Arrange
    var databaseReturnValue = new List<Car> { new Car() { Make = "ma", Model = "mo", Year = 1900 };
    var location = "testpath.ext";
    var ids = new List<int> { 1, 3, 6 };
    var expectedOutput = "Make, Model, Year\r\nma,mo,1900";

    var database = MockRepository.GenerateMock<IDatabaseProvider>();
    var storage = MockRepository.GenerateMock<IFileStorage>();

    database
       .Stub(m => m.GetCars())
       .Return(databaseReturnValue);
    storage
       .Expect(m => m.SaveText(Arg<string>.Is.Equal(location),
                               Arg<string>.Is.Equal(expectedOutput)));

    MyClass testee = new MyClass(database, storage);

    // Act
    testee.GenerateCarDetailsFile(ids, location);

    // Assert
    storage.VerifyAllExpectations();
}

SaveText您正在测试您的类的行为,以及它应该调用依赖项的事实IFileStorage。通过使用依赖注入和抽象所有辅助系统,您可以创建不会因为数据库不可访问或文件系统已满而失败的测试(请注意,这些事件可能是另一个单元测试)。

您还将创建更便携的类。将其移至具有另一种访问文件系统的方式的另一个平台时(例如,在 .NETFileStorageFileWindows Store 中),您只需创建一个特定于平台的IFileStorage实现。

所以,不要测试其他类的行为。测试你的类的行为,而不是它的依赖关系。然后使用模拟来设置那些在测试之间工作相同的依赖项的行为。

于 2013-05-23T22:42:56.897 回答
1

您的解决方案是Mocking with Dependency Injection。这样你就不会依赖任何东西。纯逻辑测试。

于 2013-05-23T21:11:38.400 回答
1

您可能希望mock使用模拟框架访问数据对象。这样您就可以对它进行单元测试,而无需依赖于实际的数据库内容,甚至不需要数据库连接。

此外,我会拆分第一种方法的“检索部分”和“保存部分”,因此您可以单独测试这些部分:

public StringWriter GenerateCarDetails(IList<int> carIds)
{

   var cars = Uow.Query<Car>().Where(x => carIds.Contains(x.Id));

   var stringWriter = new StringWriter();

   stringWriter.WriteLine("Make, Model, Year");

   foreach(var car in cars)
   {
     stringWriter.WriteLine("{0},{1},{2}", car.Make, car.Model, car.Year);
   }

   return stringWriter;
}

public void SaveToFile(StringWriter stringWriter, string location)
{
   var bytes = new System.Text.UTF8Encoding().GetBytes(stringWriter.ToString());
   var file = System.IO.File.OpenWrite(location);
   file.Write(bytes, 0, bytes.Length);
   file.Close();
}

甚至像这样:

public IEnumerable<Car> LoadCarDetails(IList<int> carIds)
{
   var cars = Uow.Query<Car>().Where(x => carIds.Contains(x.Id));
   return cars;
}

public StringWriter ConvertCarListToStrings(IEnumerable<Car> cars)
{
   var stringWriter = new StringWriter();

   stringWriter.WriteLine("Make, Model, Year");

   foreach(var car in cars)
   {
     stringWriter.WriteLine("{0},{1},{2}", car.Make, car.Model, car.Year);
   }

   return stringWriter;
}

public void SaveToFile(StringWriter stringWriter, string location)
{
   var bytes = new System.Text.UTF8Encoding().GetBytes(stringWriter.ToString());
   var file = System.IO.File.OpenWrite(location);
   file.Write(bytes, 0, bytes.Length);
   file.Close();
}

因此您至少可以ConvertCarListToStrings使用已知数据进行测试。

于 2013-05-23T21:01:23.073 回答
0

测试整个过程更像是一个集成测试。对我有用的是让测试创建一个测试数据库,将一些示例数据加载到其中,然后将数据库连接(或 ORM 表示)传递给包含您的查询的类。

调用后,GenerateCarDetailsFile您可以让测试加载文件(从传入的位置)并确保它看起来正确。然后最后让测试自行清理以删除文件和测试数据库。

另一种选择是使用良好的模拟/替换框架来模拟 DB 和/或文件 IO。我个人最喜欢的是NSubstitutehttp://nsubstitute.github.io/

于 2013-05-23T21:04:56.913 回答
0

您可以IDataProvider使用诸如GetCarsById(IEnumerable<int> ids). 然后将所有查询移至接口的实现。在需要的地方注入实现并创建用于测试的模拟实现。

于 2013-05-23T21:06:17.937 回答