如何从数据库中获取数据并保存到文件的单元测试方法是保存正确的数据

本文关键字:数据 测试方法 单元 保存 存到文件 数据库 获取 | 更新日期: 2023-09-27 18:15:18

下面是我的代码:

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

如何从数据库中获取数据并保存到文件的单元测试方法是保存正确的数据

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

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

当使用mock框架时,你创建了一个类可以使用的"伪"对象。这也意味着你应该使用依赖注入(即向你的类注入依赖)。类的依赖项似乎是数据库访问类和文件访问类。通过将它们传递给类,您也遵循了单一职责原则,简单地说,一个类应该只知道一件事。(它不应该知道如何访问数据库如何访问文件)

简单地为您需要的创建两个接口,IDatabaseRepositoryIFileStorage或沿着这些线的东西。然后将它们的实例注入到类中。当您创建单元测试时,这些很容易被模拟。例如,使用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();
}

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

您还将创建更具可移植性的类。当将其移动到具有另一种访问文件系统方式的另一个平台时(例如,在。net与Windows Store中File vs StorageFile),您只需创建一个特定于平台的IFileStorage实现。

所以,不要测试其他类的行为。相反,测试类的依赖关系的行为。然后使用mock来设置这些依赖项的行为,使其在测试之间工作相同。

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

此外,我将拆分第一个方法的"检索部分"answers"保存部分",因此您可以单独测试这些部分:

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

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

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

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

另一个选择是使用一个好的模拟/替换框架来模拟DB和/或File IO。我个人最喜欢的是NSubstitute (http://nsubstitute.github.io/)

您可以用GetCarsById(IEnumerable<int> ids)这样的方法创建一个专门的接口IDataProvider。然后将所有查询移动到接口的实现。在需要的地方注入实现,并为测试创建模拟实现。