简体   繁体   中英

unit testing public methods with tested internal methods

I find myself falling into this pattern quite a lot, I write a class composed of very small methods which are completely exercised by my unit tests. Then I find I need to build another method that calls these methods and I have to write a more complicated unit test for that – a simple example would be illustrative:

namespace FooRequest
{
    static public class Verifier
    {
        static public bool IsValid(string request)
        {
            return (!IsAllCaps(request) && !ContainsTheLetterB(request));
        }

        static internal bool IsAllCaps(string request)
        {
            return (request.Equals(request.ToUpper()));
        }

        static internal bool ContainsTheLetterB(string request)
        {
            return request.ToLower().Contains("b");
        }
    }
}

For code I'd write unit tests to cover the two internal methods like this:

namespace UnitTest
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using FooRequest;

    public class VerifierTest
    {
        [TestClass]
        public class ContainsTheLetterB
        {
            [TestMethod]
            public void ShouldReturnTrueForStringContainsB()
            {
                Assert.IsTrue(Verifier.ContainsTheLetterB("burns"));
            }

            [TestMethod]
            public void ShouldReturnFakseForStringDoesNotContainB()
            {
                Assert.IsFalse(Verifier.ContainsTheLetterB("urns"));
            }
        }

        [TestClass]
        public class IsAllCaps
        {
            [TestMethod]
            public void ShouldReturnTrueForStringIsAllCaps()
            {
                Assert.IsTrue(Verifier.IsAllCaps("IAMALLCAPS"));
            }

            [TestMethod]
            public void ShouldReturnFakseForStringDoesNotContainB()
            {
                Assert.IsFalse(Verifier.IsAllCaps("IAMnotALLCAPS"));
            }
        }
    }
}

For the public method I really just want to test “if the methods you call return false, then return false” – it's annoying that I have to set up the input in such a way to force my internal methods to return true or false – my test for this method shouldn't care about the internal methods it calls (right?)

    [TestClass]
    public class IsValid
    {
        [TestMethod]
        public void ShouldReturnFalseForInvalidStringBecauseContainsB()
        {
            Assert.IsFalse(Verifier.IsValid("b"));
        }

        [TestMethod]
        public void ShouldReturnFalseForInvalidStringBecauseIsAllCaps()
        {
            Assert.IsFalse(Verifier.IsValid("CAPS"));
        }

        [TestMethod]
        public void ShouldReturnTrueForValidString()
        {
            Assert.IsTrue(Verifier.IsValid("Hello"));
        }
    }

Obviously for this example, that's not too bad, but when there are lots of internal methods and the input is non-trivial to configure, testing my public “Is This Input Valid” method gets complicated.

Should I create an interface for all my internal methods then stub it out for the tests or is there a neater way?

I was typing a comment but it got to be too big. I think you're on the borderline of violating SRP but you are definitely violating the open/closed principle. If you need to change the way you verify strings, your verifier class needs to be modified.

I would approach this a bit different than @seldary would, but not by much...

    public interface IStringRule
    {
        bool Matches(string request);
    }

    public class AllCapsRule : IStringRule
    {
        public bool Matches(string request)
        {
            //implement
        }
    }

    public class IsContainingBRule : IStringRule
    {
        public bool Matches(string request)
        {
            //implement
        }
    }

    public class Verifier
    {
        private List<IStringRule> Rules;

        public Verifier(List<IStringRule> rules)
        {
            Rules = rules;
        }

        public bool IsValid(string request)
        {
            return (!Rules.Any(x=>x.Matches(request) == false));
        }
    }

Now your verifer is open to extension, but closed to modification. You can add as many new rules as you like and the implementation doesn't change. Testing the verifier is as simple as passing in some mock string rules that return arbitrary true and false values and making sure that the verifier returns the appropriate result.

Each IStringRule gets tested separately as you were doing.

The neater way would be as follows:

  1. Refactor your Verifier class into three classes, one for each method in this case: Verifier , AllCapsChecker , LetterBChecker .
  2. Refactor your test classes accordingly - There should be now three test classes.
  3. Inject the two inner logic classes into Verifier using your favorite DI method.
  4. The VerifierTests class should have the two dependencies arranged and injected into Verifier , and test only the Verifier logic (in this case only the logical operators).

Here you can find an adaptation of the Verifier and VerifierTests classes, just to get the idea (I used Moq here):

namespace FooRequest
{
    public interface IAllCapsChecker
    {
        bool IsAllCaps(string request);
    }

    public interface ILetterBChecker
    {
        bool IsContainingB(string request);
    }

    public class Verifier
    {
        private readonly IAllCapsChecker m_AllCapsChecker;
        private readonly ILetterBChecker m_LetterBChecker;

        public Verifier(IAllCapsChecker allCapsChecker, ILetterBChecker letterBChecker)
        {
            m_AllCapsChecker = allCapsChecker;
            m_LetterBChecker = letterBChecker;
        }

        public bool IsValid(string request)
        {
            return (!m_AllCapsChecker.IsAllCaps(request) && !m_LetterBChecker.IsContainingB(request));
        }
    }

    [TestClass]
    public class IsValid
    {
        [TestMethod]
        public void ShouldReturnFalseForInvalidStringBecauseContainsB()
        {
            var allCapsMock = new Mock<IAllCapsChecker>();
            allCapsMock.Setup(checker => checker.IsAllCaps("example")).Returns(true);

            var letterBChecker = new Mock<ILetterBChecker>();
            letterBChecker.Setup(checker => checker.IsContainingB("example")).Returns(true);

            var verifier = new Verifier(allCapsMock.Object, letterBChecker.Object);

            Assert.IsFalse(verifier.IsValid("example"));
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM