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

时间:2013-05-23 20:56:06

标签: c# unit-testing

这是我的代码:

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,然后将它们保存到文件中。

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

我无法想象如何做到这一点,因为两种方法都返回无效。

5 个答案:

答案 0 :(得分:2)

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

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

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

只需为您需要的内容创建两个界面,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();
}

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

您还将创建更具可移植性的类。将此移动到具有另一种访问文件系统的平台的另一种平台(例如,在.NET与Windows应用商店中File vs StorageFile)时,您只需创建一个特定于平台的IFileStorage实现。 / p>

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

答案 1 :(得分: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

答案 2 :(得分:1)

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

答案 3 :(得分:0)

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

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

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

答案 4 :(得分:0)

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