6
\$\begingroup\$

I started writing a set of tests for my SageContextBase class, which needs to be abstract because the client code must derive from it (think of it as EF's DbContext - it can't be instantiated directly, but it packs a truckload of functionality) - I have 5 passing tests covering everything that should be happening inside the Open method:

[TestClass]
public class SageContextBaseOpenTests
{
    private Mock<IView> GetMockView(string viewId)
    {
        var view = new Mock<IView>();
        view.SetupAllProperties();
        view.SetupGet(m => m.ViewId).Returns(viewId);
        view.Setup(m => m.Compose(It.IsAny<IView[]>()));
        view.Setup(m => m.Dispose());
        view.Setup(m => m.Insert());
        view.Setup(m => m.Delete());
        view.Setup(m => m.Update());
        view.Setup(m => m.FilterCount(It.IsAny<string>()));

        return view;
    }

    private Mock<ISession> GetMockSession()
    {
        var dbLink = new Mock<IDbLink>();
        dbLink.SetupAllProperties();

        const string headersViewId = "PO0620";
        var headersView = GetMockView(headersViewId);
        dbLink.Setup(m => m.OpenView(headersViewId)).Returns(headersView.Object);

        const string detailsViewId = "PO0630";
        var detailsView = GetMockView(detailsViewId);
        dbLink.Setup(m => m.OpenView(detailsViewId)).Returns(detailsView.Object);

        var session = new Mock<ISession>();
        session.SetupAllProperties();
        session.Setup(m => m.OpenDbLink(It.IsAny<DBLinkType>(), It.IsAny<DBLinkFlags>())).Returns(dbLink.Object);

        return session;
    }

    [TestMethod]
    public void Open_InitializesSessionWithSpecifiedAppInfo()
    {
        // arrange
        var credentials = new SageCredential("testuser", "testpwd", "testdb");
        var appInfo = new SageAppInfo("testapp", "v42");

        var session = GetMockSession();
        var context = new TestSageContext(credentials, appInfo, session.Object);

        // act
        context.Open();

        // assert
        session.Verify(m => m.Init(It.IsAny<string>(), appInfo));
    }

    [TestMethod]
    public void Open_OpensSessionWithSpecifiedCredentials()
    {
        // arrange
        var credentials = new SageCredential("testuser", "testpwd", "testdb");
        var appInfo = new SageAppInfo("testapp", "v42");

        var session = GetMockSession();
        var context = new TestSageContext(credentials, appInfo, session.Object);

        // act
        context.Open();

        // assert
        session.Verify(m => m.Open(credentials, It.IsAny<DateTime>(), It.IsAny<int>()));
    }

    [TestMethod]
    public void Open_OpensDbLinkWithCompanyAndReadWriteFlags()
    {
        // arrange
        var credentials = new SageCredential("testuser", "testpwd", "testdb");
        var appInfo = new SageAppInfo("testapp", "v42");

        var session = GetMockSession();
        var context = new TestSageContext(credentials, appInfo, session.Object);

        // act
        context.Open();

        // assert
        session.Verify(m => m.OpenDbLink(DBLinkType.Company, DBLinkFlags.ReadWrite));
    }

    [TestMethod]
    public void Open_CreatesViewSetInstances()
    {
        // arrange
        var credentials = new SageCredential("testuser", "testpwd", "testdb");
        var appInfo = new SageAppInfo("testapp", "v42");

        var session = GetMockSession();
        var context = new TestSageContext(credentials, appInfo, session.Object);

        if (context.ViewSets.Any())
        {
            Assert.Inconclusive("Context.ViewSets collection must initialize empty.");
        }

        var properties = typeof (TestSageContext).GetProperties()
            .Where(property => property.PropertyType.IsGenericType
                               && property.CanRead && property.CanWrite
                               && property.GetMethod.IsPublic
                               && property.PropertyType.IsGenericType
                               && property.PropertyType.GetGenericTypeDefinition().Name == typeof(ViewSet).Name + "`1")
            .ToList();

        var expected = properties.Count;

        // act
        context.Open();

        // assert
        Assert.AreEqual(expected, context.ViewSets.Count);
    }

    [TestMethod]
    public void Open_CallsOnModelCreating()
    {
        // arrange
        var credentials = new SageCredential("testuser", "testpwd", "testdb");
        var appInfo = new SageAppInfo("testapp", "v42");

        var session = GetMockSession();
        var context = new TestSageContext(credentials, appInfo, session.Object);

        // act
        context.Open();

        // assert
        Assert.IsTrue(context.IsModelComposed);
    }
}

These tests use a fake implementation of the abstract class I'm testing:

class TestSageContext : SageContextBase
{
    public TestSageContext(SageCredential credential, SageAppInfo appInfo, ISession session) 
        : base(credential, appInfo, session)
    {
    }

    public ViewSet<PO0620> PurchaseOrderHeaders { get; set; }
    public ViewSet<PO0630> PurchaseOrderDetails { get; set; }

    public bool IsModelComposed { get; private set; }

    protected override void OnModelComposing()
    {
        PurchaseOrderHeaders.Compose(null, PurchaseOrderDetails, null, null, null);
        PurchaseOrderDetails.Compose(PurchaseOrderHeaders, null, null, null, null, null);

        IsModelComposed = true;
    }
}

Here's the method under test, and a few relevant members - note that the entire class is up for review here:

    /// <summary>
    /// Opens the database connection and initializes the <see cref="ViewSet{TEntity}"/> members.
    /// </summary>
    public void Open()
    {
        _session.Init(string.Empty, _info);
        _session.Open(_credential, DateTime.Now);
        _db = _session.OpenDbLink(DBLinkType.Company, DBLinkFlags.ReadWrite);

        var properties = GetType().GetProperties()
            .Where(p => p.CanRead && p.CanWrite
                        && p.PropertyType.IsGenericType
                        && p.CanRead
                        && p.CanWrite
                        && p.GetMethod.IsPublic
                        && p.PropertyType.IsGenericType
                        && p.PropertyType.GetGenericTypeDefinition().Name == typeof (ViewSet).Name + "`1");

        foreach (var propertyInfo in properties)
        {
            var entityType = propertyInfo.PropertyType.GenericTypeArguments[0];
            var view = AddView(GetMappedViewId(entityType));

            var constructedType = typeof (ViewSet<>).MakeGenericType(entityType);
            var viewSetInstance = (ViewSet)Activator.CreateInstance(constructedType, view, this);

            propertyInfo.SetValue(this, viewSetInstance);
            _viewSets.Add(viewSetInstance);
        }

        OnModelComposing();
    }

    private readonly IList<ViewSet> _viewSets = new List<ViewSet>(); 
    public IReadOnlyList<ViewSet> ViewSets { get { return _viewSets.ToList(); } }

    /// <summary>
    /// Provides a hook for derived types to specify how views are composed.
    /// </summary>
    protected abstract void OnModelComposing();

I'm particularly interested in the implementation of the last test, which verifies whether the OnModelComposing method was called... but as always, any & all feedback is welcome.

\$\endgroup\$
  • \$\begingroup\$ What I typically do is thoroughly test the first implementation of an abstract class and leave a comment explaining why the other ones aren't as thoroughly tested. FWIW. For some reason, I'd never thought of creating a dummy implementation to test the base. ++ \$\endgroup\$ – RubberDuck Mar 1 '16 at 9:07
  • 1
    \$\begingroup\$ Rubberduck in this case the implementing code is client code that's not yet written (except some dummy F5-test console app code); I want to test the guts out of this one before I actually use it. \$\endgroup\$ – Mathieu Guindon Mar 1 '16 at 12:20
  • \$\begingroup\$ It gets harder and harder to review your code. \$\endgroup\$ – RubberDuck Mar 2 '16 at 10:29
  • \$\begingroup\$ Open_CallsOnModelCreating looks like it should really be Open_CallsOnModelComposing something to watch during refactoring possibly... \$\endgroup\$ – forsvarir Mar 10 '16 at 9:20
3
\$\begingroup\$

Just a couple of things.

You repeat this set up in every test.

    var credentials = new SageCredential("testuser", "testpwd", "testdb");
    var appInfo = new SageAppInfo("testapp", "v42");

Extract the fixtures into fields and create a [TestInitialize] routine. If you need to change them later, then you have a different fixture and should create a new test class anyway. It helps to keep your test classes small, focused, and clean.

The It.IsAny() method is useful for getting your test to initially pass, but I get hesitant when I see it sticking around in code that's "done". I get doubly hesitant when I see it in a Verify().

session.Verify(m => m.Init(It.IsAny<string>(), appInfo));

It could be perfectly ok, but always worth a second look. It's a balancing act between making sure the tests go red if a breaking change is made, and not testing the implementation.

In a perfect world, we'd never have to Verify() anything. We would just Assert the output. We don't live in a perfect world, so this all might be fine, but it's definitely worth reflecting on.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.