views:

185

answers:

4

I am tasked with pulling all the rows from a 3rd party vendor's SQLite data table, creating business objects from those records, and sending the new business objects off to another class.

Pseudo-code:

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

I've got all that working.

My question is: How can I write this class so it's unit testable?

Should I:

  • use the repository pattern to mock out the data access
  • actually provide a dummy SQLite database in the unit test

Or any better ideas? I'm using C#, but this question seems rather language-agnostic.

+1  A: 

Well, the only thing that would really need to be tested here is TransformIntoBizObjs, I would think, since the connection code should have been written/tested elsewhere. Simply passing things that might show up to Transform and seeing if the right thing pops out would be what you need to do.

Remember to test all usecases of Transform, even potentially weird items that probably shouldn't end up in the function call, but might. Never know what people have been shoving in their databases.

Kurisu
Right, TransformIntoBizObjs is what needs to be tested. How do I test that knowing the code requires opening a SQLite db connection?
Judah Himango
It requires a database connection? It really should not. Your steps should be 1) Get the records from SQLite into memory 2) Transform the records into the new entities 3) Have fun with the entities. In this case you could just create in-memory example records and feed them into the transfomation method.
Daniel Brückner
Exactly, you don't need to test any of the code you showed us. You create a separate method for testing, that simply calls Transform on it's own. How/Where you create that method depends on what kind of testing philosophy you're following.
Kurisu
Imagine this whole thing runs asynchronously and raises an event. Imagine the TransformIntoBizObjs is dealing with hierarchical data. These parts should be tested.
Judah Himango
Daniel and Kurisu, you've given me some good ideas. Thank you.
Judah Himango
A: 

Databases are complicates, you need to test your query code and you need to test it against a real sqlite instance - otherwise you can't be sure you didn't hit some rare sqlite quirk or bug.

And since the only way to test your query is to run it on a real sqlite file, and it's really easy to include such a file with your test there's no point to adding another layer just to make it "more" testable or to have "pure" unit tests.

Just make sure to add all the strange edge cases you can think of to your sample file.

Nir
A: 

Inversion of Control (IoC) and Dependency Injection (DI) will go a long way towards making your code more testable. There are a lot of frameworks out there that can assist you with this, but for your purposes you don't necessarily need to go to all that effort.

Start with extracting an interface that might look something like this:

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

}

Once you've done that, you should refactor the OpenSqlLiteConnection() method to return an instance of ISqlLiteConnection, rather than the concrete implementation. To test, just create a class that implements your interface, which mocks up the actual DB queries and connections with determinate results.

Josh E
I thought about that. But some pretty wise devs have said, "Don't mock System.Data.*!" http://ayende.com/Blog/archive/2006/07/02/DoNOTMockSystemData.aspx
Judah Himango
+1  A: 

You could inject a test-only Sqlite database quite easily, refactoring the code to look like below. But how are you asserting the results? The business objects are passed to someOtherClass. If you inject an ISomeOtherClass, that class's actions need to be visible too. It seems like a bit of pain.

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 */);
    }
}

Using an IRepository would remove some of the code from this class, allowing you to mock the IRepository implementation, or fake one just for test.

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 */);
    }
}

But this still feels quite cumbersome. There are two reasons, 1) the Repository pattern has been getting some bad press lately from Ayende, who knows a thing or two about Repository. And 2) what are you doing writing your own data access!? Use NHibernate and 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 */);
    }
}

The result is a much smaller class and a business object and data-layer that you can change more easily. And you don't even have to mock the database calls, you can configure and initialize ActiveRecord to use a test database (in-memory, even).

Anthony Mastrean
I know Ayende is cautious using IRepository. I consulted that post before asking a question here on SO. I figured I could use NHibernate or LINQ-to-Sqlite, but this felt like overkill -- this is a one-time-use class (e.g. file->import...) that talks exactly once to a 3rd party database. Setting up ORM to do such a simple thing seems like overkill.
Judah Himango
Anyways, thank you for this answer. I've voted it up. It helps me with my problem and gives me some ideas, even though I probably won't go with AR or NHibernate or other ORM.
Judah Himango
How you do anything is how you do everything.
Anthony Mastrean
And ActiveRecord is actually -very- easy to get started. I would check out the video here <http://tr.im/jQgd>.
Anthony Mastrean