Написание тестируемого класса «импорт данных из базы данных» - PullRequest
2 голосов
/ 04 июня 2009

Мне поручено извлечь все строки из таблицы данных SQLite стороннего поставщика, создать бизнес-объекты из этих записей и отправить новые бизнес-объекты в другой класс.

Псевдо-код:

var databasePath = "%user profile%\application data\some3rdPartyVendor\vendor.sqlite"
var connection = OpenSqliteConnection(databasePath);
var allGizmoRecords = connection.Query(...);
var businessObjects = TransformIntoBizObjs(allGizmoRecords);
someOtherClass.HandleNewBizObjs(businessObjects);

У меня все это работает.

Мой вопрос: Как я могу написать этот класс, чтобы он тестировался на модуле?

Должен ли я:

  • использовать шаблон репозитория для моделирования доступа к данным
  • фактически предоставляет фиктивную базу данных SQLite в модульном тесте

Или есть идеи получше? Я использую C #, но этот вопрос кажется не зависящим от языка.

Ответы [ 4 ]

2 голосов
/ 05 июня 2009

Вы можете внедрить базу данных Sqlite только для тестирования, реорганизовав код так, как показано ниже. Но как вы утверждаете результаты ? Бизнес-объекты передаются в someOtherClass. Если вы введете ISomeOtherClass, действия этого класса также должны быть видны. Это похоже на небольшую боль.

public class KillerApp
{
    private String databasePath;
    private ISomeOtherClass someOtherClass;

    public KillerApp(String databasePath, ISomeOtherClass someOtherClass)
    {
        this.databasePath = databasePath;
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        var connection = OpenSqliteConnection(databasePath);
        var allGizmoRecords = connection.Query(...);
        var businessObjects = TransformIntoBizObjs(allGizmoRecords);
        someOtherClass.HandleNewBizObjs(businessObjects);
    }
}

[TestClass]
public class When_Doing_That_Thing
{
    private const String DatabasePath = /* test path */;
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        app = new KillerApp(DatabasePath, someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

Использование IRepository приведет к удалению некоторого кода из этого класса, что позволит вам смоделировать реализацию IRepository или подделку только для проверки.

public class KillerApp
{
    private IRepository<BusinessObject> repository;
    private ISomeOtherClass someOtherClass;

    public KillerApp(IRepository<BusinessObject> repository, ISomeOtherClass someOtherClass)
    {
        this.repository = repository;
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        BusinessObject[] entities = repository.FindAll();
        someOtherClass.HandleNewBizObjs(entities);
    }
}

[TestClass]
public class When_Doing_That_Thing
{
    private const String DatabasePath = /* test path */;
    private IRepository<BusinessObject> repository;
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        repository = new BusinessObjectRepository(DatabasePath);
        app = new KillerApp(repository, someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

Но это все еще кажется довольно громоздким. Есть две причины: 1) шаблон Репозитария недавно получил плохую прессу от Айенде , который знает кое-что о Репозитории. И 2) что вы делаете написание собственного доступа к данным !? Используйте NHibernate и ActiveRecord !

[ActiveRecord] /* You define your database schema on the object using attributes */
public BusinessObject
{
    [PrimaryKey]
    public Int32 Id { get; set; }

    [Property]
    public String Data { get; set; }

    /* more properties */
}

public class KillerApp
{
    private ISomeOtherClass someOtherClass;

    public KillerApp(ISomeOtherClass someOtherClass)
    {
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        BusinessObject[] entities = BusinessObject.FindAll() /* built-in ActiveRecord call! */
        someOtherClass.HandleNewBizObjs(entities);
    }
}

[TestClass]
public class When_Doing_That_Thing : ActiveRecordTest /* setup active record for testing */
{
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        app = new KillerApp(someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

В результате получается гораздо меньший класс, а также бизнес-объект и слой данных, которые вы можете легко изменить. И вам даже не нужно имитировать вызовы базы данных, вы можете настроить и инициализировать ActiveRecord для использования тестовой базы данных (даже в памяти).

1 голос
/ 04 июня 2009

Ну, единственное, что действительно нужно было бы здесь протестировать, это TransformIntoBizObjs, я думаю, поскольку код соединения должен был быть написан / протестирован в другом месте. Вам нужно просто передать вещи, которые могут появиться в Transform, и посмотреть, появится ли нужная вещь.

Не забудьте протестировать все варианты использования Transform, даже потенциально странные элементы, которые, вероятно, не должны заканчиваться вызовом функции, но могут. Никогда не знаешь, что люди пихали в свои базы данных.

0 голосов
/ 05 июня 2009

Inversion of Control (IoC) и Dependency Injection (DI) будут иметь большое значение для того, чтобы сделать ваш код более тестируемым. Есть много рамок, которые могут помочь вам в этом, но для ваших целей вам не обязательно идти на все эти усилия.

Начните с извлечения интерфейса, который может выглядеть примерно так:

Interface ISqlLiteConnection
{
     public IList<GizmoRecord> Query(...);

}

Как только вы это сделаете, вам следует провести рефакторинг метода OpenSqlLiteConnection (), чтобы он возвращал экземпляр ISqlLiteConnection, а не конкретную реализацию. Чтобы проверить, просто создайте класс, который реализует ваш интерфейс, который макетирует реальные запросы к БД и соединения с определенными результатами.

0 голосов
/ 05 июня 2009

Базы данных сложны, вам нужно протестировать код запроса и проверить его на реальном экземпляре sqlite - иначе вы не можете быть уверены, что не столкнулись с какой-то редкой причудой или ошибкой sqlite.

И поскольку единственный способ протестировать ваш запрос - это запустить его на реальном файле sqlite, и очень просто включить такой файл в ваш тест, нет смысла добавлять другой слой просто для того, чтобы сделать его «более» тестируемым или иметь «чистые» юнит-тесты.

Просто добавьте все странные крайние случаи, которые вы можете придумать, в ваш файл примера.

...