2

I've got data access layer that has two types of method GetLatestX and GetX. GetLatestX looks something like this:

    public IElementType GetLatestElementType(Guid id)
    {
        IElementType record = null;
        using (DatabaseSession session = CreateSession())
        {
            record = session.Connection.Get<ElementTypeRecord>(id);
        }
        return record;
    }

That's reasonably easy to unit test.

However, GetX wraps this GetLatest in a RefCount Observable and emits new values in response to a messaging system. Testing this method is a lot more complex. I want to check the following, rather complex behavior:

  1. You can subscribe and it retrieves a value from the database
  2. It starts listening for messages
  3. Subscribing again doesn't result in a repeated database call
  4. When the mock message system simulates a message a new database access is called and the subscriptions get the new versions. Only one additional database call is used.
  5. Unsubscribing the second subscription doesn't result in the system stopping listening to messages.
  6. Unsubscribing to the first subscription results in the disposal of resources, and un-subscription from the messages.

So, I've got all this in a single unit test, which is hideous. However, I'm not sure how I could break it up. I could only test 1, but to test 2 I'd have to go through 1 and 2, for 3 I'd still have to go through steps 1, 2, 3 etc. So I'd just be copying the same giant test, but having Asserts in different places each time.

And the code I'm testing in this method:

    public IObservable<IElement> GetElement(Guid id)
    {
        return CreateObservableFor(GetLatestElement(id), GetLatestElement);
    }

It's a single line, half of which has been tested earlier. The other half is private:

    private IObservable<T> CreateObservableFor<T>(T entity, Func<Guid, T> getLatest)
    {
        Guid id = (entity as ConfigurationEntity).ID;

        //return subject;
        return Observable.Create<T>(observer =>
        {
            // publish first value
            observer.OnNext(entity);

            // listen to internal or external update notifications from messages
            Action<ConfigurationMessage> callback = (message) =>
            {
                // TODO, check timestamp is after previous?

                // use callback to get latest value
                observer.OnNext(getLatest(id));
            };

            messageService.SubscribeToConfiguration(id.ToString(), callback);

            // if it has been completed and stop listening to messages
            return Disposable.Create(() =>
            {
                Console.WriteLine("Unsubscribing from topic " + id);
                messageService.UnsubscribeToConfiguration(id.ToString(), callback);
            });
        }).Publish().RefCount();
    }

But it behaves the same way for all the GetX methods.

My first thought is I should split the GetLatestX into an interface I can test separately then mock - but that seems to split the data access class into two for no good reason other than unit tests. They don't really conceptually belong as separate units in my mind. Is there another way of 'mocking' this dependency within a class? Or should I just split them up for the sake of testing?

In the same vein, testing the functionality of GetX is effectively repeatedly testing the logic of CreateObservableFor. I see why I should be testing each API method rather than what is really the internals of the API (in case something changes), but it seems so... inefficient.

How can I structure this unit test in a better way?

Example test:

    [Test]
    public void GetElementTypeTest()
    {
        // test data
        var id = Guid.NewGuid();
        var nameA = "TestNameA";
        var nameB = "TestNameB";

        // mock database
        var columnNames = new[] { "ID", "Name" };

        // data values A will be the first set of data returned, and after configuration update B will be returned
        var dataValuesA = new List<object[]>();
        dataValuesA.Add(new object[] { id, nameA });

        var dataValuesB = new List<object[]>();
        dataValuesB.Add(new object[] { id, nameB });

        mockDbProviderFactory = new MockDbProviderFactory()
            .AddDatareaderCommand(columnNames, dataValuesA)
            .AddDatareaderCommand(columnNames, dataValuesB);

        // test method
        IEMF emf = new EMF(mockMessageService.Object, new MockHistorian(), mockDbProviderFactory.Object, "");

        var resultObservable = emf.GetElementType(id);

        // check subscription to config changes has not occurred and database not accessed
        mockDbProviderFactory.Verify(f => f.CreateConnection(), Times.Once);
        mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Never);

        //subscribe to observable
        int sub1Count = 0;
        var subscription = resultObservable.Subscribe(result => {
            sub1Count++;

            // check result
            Assert.AreEqual(new ElementTypeRecord(id, (sub1Count == 1 ? nameA : nameB)), result, "Result from EMF does not match data");

        });


        // check subscribed to config changes and subscription called
        Assert.IsTrue(sub1Count == 1, "Subscription not called");
        mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);

        // check we've subscribed with our id
        Assert.AreEqual(this.configCallbacks[0].Item1, id.ToString(), "Unexpected message system subscription topic");

        // open a second, short term subscription and ensure that the system does not re-subscribe to updates, or read the data again
        int sub2Count = 0;
        resultObservable.Take(1).Subscribe(result => {
            sub2Count++;

            // check result (should be second data item)
            Assert.AreEqual(new ElementTypeRecord(id, nameB), result, "Result from EMF does not match data");
        });

        // check subscribed to config changes has not changed
        mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);

        //emit a new value by simulating a configuration change message
        this.configCallbacks[0].Item2(new ConfigurationMessage(DateTime.Now));

        // check subscriptions called
        Assert.IsTrue(sub1Count == 2, "Subscription not called");
        Assert.IsTrue(sub2Count == 1, "Subscription not called");

        // unsubscribe
        mockMessageService.Verify(ms => ms.UnsubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Never);
        subscription.Dispose();

        // verify subscription removed
        mockMessageService.Verify(ms => ms.UnsubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);
        Assert.IsTrue(this.configCallbacks.Count == 0, "Unexpected message system unsubscription topic");

        // validate that the connection, command and reader were used correctly
        mockDbProviderFactory.Verify(f => f.CreateConnection(), Times.Exactly(2));
        mockDbProviderFactory.MockConnection.Verify(c => c.Open(), Times.Exactly(2));
        mockDbProviderFactory.MockConnection.Verify(c => c.Close(), Times.Exactly(2));

        //first data call
        mockDbProviderFactory.MockCommands[0].Verify(c => c.PublicExecuteDbDataReader(It.IsAny<CommandBehavior>()), Times.Once);
        mockDbProviderFactory.MockCommands[0].MockDatareader.Verify(dr => dr.Read(), Times.Exactly(2));

        //second data call
        mockDbProviderFactory.MockCommands[1].Verify(c => c.PublicExecuteDbDataReader(It.IsAny<CommandBehavior>()), Times.Once);
        mockDbProviderFactory.MockCommands[1].MockDatareader.Verify(dr => dr.Read(), Times.Exactly(2));
    }

1 Answer 1

2

This answer is going to be somewhat generic because I'm looking more at the principle than your specific case.

In my view, the first thing you want to do is create a series of helper methods. You can extract these to a test library class or keep them in your test class. The idea is that the helper methods do the work, and your tests call them.

For each of your six behaviors, you have a separate test, each calling the relevant helpers.

I'd also, for the sake of clarity, use a test code pattern: I personally prefer the Arrange-Act-Assert pattern, but if there's a different one you prefer, that's fine.

This structure lets you keep your asserts to one per test method, and reduces duplicate code with similar setups.

Some pseudo-code-ish examples (C#-ish)

[Test]
public void SubscribeReturnsValue()
{
    //Arrange
    var service = HelperLib.CreateService(); // you're creating a real service here, but with a mock data provider set up in your helper library
    var expectedResult = ; //whatever you expect the service to return

    //Act
    var result = service.Subscribe(); 

    //Assert
    Assert.That(result == expectedResult);
}

[Test]
public void ServiceStartsListening()
{
    //Arrange
    var service = HelperLib.CreateMockService(); 

    //Act 
    var result = service.Subscribe();

    //Assert
    Assert.That(service.IsListening());
}

[Test]
public void ServiceGetsNew()
{
    //Arrange
    var service = HelperLib.CreateMockService(); 
    var subscription= service.Subscribe();

    //Act 
    service.NewMessage();

    //Assert
    Assert.That(subscription.MessageCount == 2);
}

In essence, you're keeping your test code clean so it's obvious what it's testing, and asserting on exactly one condition. Because your helper library is doing the heavy lifting of creating mock objects and repositories, it stops mattering that you have getLatestX() called by getX() - you test both via the helper library and look for the specifics you need to work with.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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