-1

I'm wondering about Unit Tests. Let say i got a code ( in C#, but language is not important here):

public class SOT: ISOT
{
    List<string> _internalCollection = new List<string>();
    public string CurrentCollectionName { get; }

    public void AddItem(string item)
    {
        _internalCollection.Add(item);
    }

    public void ChangeCollection()
    {
        _internalCollection.Clear();
    }

    public List<string> GetCollectionItems()
    {
        return new List<string>(_internalCollection);
    }
}

and we want to test it:

public class SOT_Test
{
    [Fact]
    public void GetCollectionItem_ReturnAllAddedItem()
    {
        var onTest = new SOT();
        onTest.AddItem("first");
        onTest.AddItem("second");

        var result = onTest.GetCollectionItems();
        Assert.Collection(result, e=>e.Equals("first"), e=>e.Equals("second"));

    }

    [Fact]
    public void GetCollectionItem_CollectionChanged_ReturnOnlyFromNewCollection()
    {
        var onTest = new SOT();
        onTest.AddItem("first");
        onTest.AddItem("second");
        onTest.ChangeCollection();
        onTest.AddItem("3th");
        onTest.AddItem("4th");

        var result = onTest.GetCollectionItems();
        Assert.Collection(result, e => e.Equals("3th"), e => e.Equals("4th"));
    }
}

But after some time, few more feature and refactors, out SOT looks more like this:

public interface ICollectionManager { string GetNewCollection(); }

public class ColectionManager: ICollectionManager
{
    public string GetNewCollection()
    {
        return Guid.NewGuid().ToString();
    }
}

public interface IItemManager
{
    void Add(string item);
    void Clear();
    IEnumerable<string> GetItems();
}

public class ItemManager: IItemManager
{
    List<string> _items = new List<string>();
    public void Add(string item)
    {
        _items.Add(item);
    }

    public void Clear()
    {
        _items.Clear();
    }

    public IEnumerable<string> GetItems()
    {
        return new List<string>(_items);
    }
}

public class SOT: ISOT
{
    private IItemManager _itemManager;
    private ICollectionManager _collectionManager;

    public SOT2(IItemManager itemManager, ICollectionManager collectionManager)
    {
        _itemManager = itemManager;
        _collectionManager = collectionManager;
    }

    public void AddItem(string item)
    {
        _itemManager.Add(item);
    }

    public void ChangeCollection()
    {
        var collection = _collectionManager.GetNewCollection();
        _itemManager.Clear();
        //... Do something with collection 
    }

    public List<string> GetCollectionItems()
    {
        return new List<string>(_itemManager.GetItems());
    }
}

So, in our test, we should Mock both dependences in test to focus on testing logic in this particular unit, or rather keep tests as they are with only small adjustment:

public class SOT_Tests
{
    [Fact]
    public void GetCollectionItem_ReturnAllAddedItem()
    {
        var onTest = new SOT2(new ItemManager(), new ColectionManager());
        onTest.AddItem("first");
        onTest.AddItem("second");

        var result = onTest.GetCollectionItems();
        Assert.Collection(result, e => e.Equals("first"), e => e.Equals("second"));

    }

    [Fact]
    public void GetCollectionItem_CollectionChanged_ReturnOnlyFromNewCollection()
    {
        var onTest = new SOT2(new ItemManager(), new ColectionManager());
        onTest.AddItem("first");
        onTest.AddItem("second");
        onTest.ChangeCollection();
        onTest.AddItem("3th");
        onTest.AddItem("4th");

        var result = onTest.GetCollectionItems();
        Assert.Collection(result, e => e.Equals("3th"), e => e.Equals("4th"));
    }
}

Tests are still working, and are still correct, but now they are penetrating two layers of code. So ther are more like integration tests. But if we just mock dependences:

public class SOT_Tests_Alt
{
    [Fact]
    public void GetCollectionItem_ReturnAllAddedItem()
    {
        var itemManagerMock = new Mock<IItemManager>();
        var collectionManagerMock = new Mock<ICollectionManager>();

        var onTest = new SOT2(itemManagerMock.Object, collectionManagerMock.Object);
        onTest.AddItem("first");
        onTest.AddItem("second");
        itemManagerMock.Setup(e => e.GetItems()).Returns(new List<string>() {"first", "second"});

        var result  = onTest.GetCollectionItems();

        Assert.Collection(result, e => e.Equals("first"), e => e.Equals("second"));

    }

    [Fact]
    public void GetCollectionItem_ShouldAllAddedItemBePassedToItemManager()
    {
        var itemManagerMock = new Mock<IItemManager>();
        var collectionManagerMock = new Mock<ICollectionManager>();

        var onTest = new SOT2(itemManagerMock.Object, collectionManagerMock.Object);
        onTest.AddItem("first");
        onTest.AddItem("second");

        onTest.GetCollectionItems();

        itemManagerMock.Verify(e => e.Add(It.IsAny<string>()), Times.Exactly(2));

    }



    [Fact]
    public void GetCollectionItem_CollectionChanged_ShouldCollectionManagerBeCalled()
    {
        var itemManagerMock = new Mock<IItemManager>();
        var collectionManagerMock = new Mock<ICollectionManager>();
        var onTest = new SOT2(itemManagerMock.Object, collectionManagerMock.Object);
        onTest.AddItem("first");
        onTest.AddItem("second");
        onTest.ChangeCollection();
        onTest.AddItem("3th");
        onTest.AddItem("4th");

        onTest.GetCollectionItems();
        collectionManagerMock.Verify(e=>e.GetNewCollection(),Times.Once);
    }
}

We end with testing method internals, what is well, not as good as testing system output, or we just test logic that we just mocked, what is also pretty dump i think.

So, what approach is correct one?

  • No time to write an answer but have a read of this: destroyallsoftware.com/blog/2014/… – Ant P Jan 10 at 11:46
  • In essence though you can avoid mocking by having your external dependencies separated completely from your "domain" logic - e.g. call your DB, pass the results into the domain as values, unit test the domain mock-free. From then on, just write unit tests at the level of granularity that makes sense. You don't need to mock out implementation details (e.g. testing a mean calculation function wouldn't mock out the division operation, would it?) – Ant P Jan 10 at 12:02
1

If ItemManager and CollectionManager are directly calling external system that you don't setup nor clean up, then you would want to go with those mocks. However, I see them as fakes and setup tests something like:

public interface IGetItemsFromExternalSystem //your IItemManager
{
    void Add(string item);
    void Clear();
    IEnumerable<string> GetItems();
}

internal class FakeExternalSystemWithStuff : IGetItemsFromExternalSystem
{
    private readonly List<string> fakeItems = new List<string>();

    public FakeExternalSystemWithStuff()
    {
        //You can add items here, but your test is oblivious to question:
        //How on earth can you change the collection?
        //fakeItems.Add("fake1");
        //fakeItems.Add("fake2");
    }

    internal void AddFakeItem(string item)
    {
        //Populate items in tests, if you feel it is worth the effort.
        //IMO it would make sense if item really means something. E. g. AddFakeProductToBasket or AddFakeUploadedImage
        fakeItems.Add(item);
    }

    void IGetItemsFromExternalSystem.Add(string item)
    {
        throw new NotImplementedException();
    }

    void IGetItemsFromExternalSystem.Clear()
    {
        throw new NotImplementedException();
    }

    IEnumerable<string> IGetItemsFromExternalSystem.GetItems()
    {
        return fakeItems.ToArray();
    }
}

public class SystemUnderTest //your SOT2
{
    private readonly IGetItemsFromExternalSystem items;

    public SystemUnderTest(IGetItemsFromExternalSystem items)
    {
        this.items = items;
    }

    public void AddItem(string item)
    {
        items.Add(item);
    }

    public void ChangeCollection()
    {
        items.Clear();
    }

    public IEnumerable<string> GetCollectionItems()
    {
        return items.GetItems();
    }
}

public void ItemsCanBeRetreivedFromCollection()
{
    var fakeItems = new FakeExternalSystemWithStuff();
    fakeItems.AddFakeItem("fake1");
    var sut = new SystemUnderTest(fakeItems);
    var items = sut.GetCollectionItems();
    Assert.True(items.Contains("fake1"));
}

If your ItemManager and ColectionManager won't call external system, then instantiate them in ctor. They are internals of SOT2, if you keep them inside SOT2, then your tests test behavior not them implementation and therefore allow you to change the implementation if needed.

public SOT2()
{
    _itemManager = new ItemManager();
    _collectionManager = new ColectionManager();
}

All in all I see that currently your business layer is thin and value of such testing is small if not negative (taking test suite maintenance into consideration). If it simply is getting and adding items (strings) to collection then I wouldn't write those tests.

  • those "business layer" is not any real life case, just example i wrote to clarify question ;) – Thaven Jan 10 at 14:18
  • I would happily elaborate more on my answer if you feel the need for it. – Siim Haas Jan 10 at 14:29

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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