【问题标题】:unit testing public methods with tested internal methods使用经过测试的内部方法对公共方法进行单元测试
【发布时间】:2012-10-09 03:53:57
【问题描述】:

我发现自己经常陷入这种模式,我编写了一个由非常小的方法组成的类,这些方法完全由我的单元测试执行。然后我发现我需要构建另一个调用这些方法的方法,并且我必须为此编写一个更复杂的单元测试——一个简单的例子就可以说明问题:

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");
        }
    }
}

对于代码,我会编写单元测试来覆盖两个内部方法,如下所示:

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"));
            }
        }
    }
}

对于公共方法,我真的只想测试“如果您调用的方法返回 false,则返回 false”——令人讨厌的是,我必须以这样一种方式设置输入,以强制我的内部方法返回 true 或false - 我对此方法的测试不应该关心它调用的内部方法(对吗?)

    [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"));
        }
    }

显然对于这个例子来说,这并不算太糟糕,但是当有很多内部方法并且输入的配置不是很重要时,测试我的公共“此输入是否有效”方法会变得很复杂。

我应该为我的所有内部方法创建一个接口,然后将其存根用于测试还是有更简洁的方法?

【问题讨论】:

    标签: c# .net unit-testing


    【解决方案1】:

    我正在输入评论,但它太大了。我认为您处于违反 SRP 的边缘,但您肯定违反了开放/封闭原则。如果您需要更改验证字符串的方式,则需要修改您的验证程序类。

    我会和@seldary 有点不同,但差别不大......

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

    现在您的验证程序可以扩展,但不能修改。您可以根据需要添加任意数量的新规则,并且实施不会改变。测试验证器就像传入一些返回任意真假值的模拟字符串规则一样简单,并确保验证器返回适当的结果。

    每个 IStringRule 都会像您一样单独测试。

    【讨论】:

    • +1,我喜欢这种方法。确实遵守开放/封闭原则,添加规则将是一项低摩擦的任务。
    【解决方案2】:

    更简洁的方法如下:

    1. Verifier 类重构为三个类,在这种情况下,每个方法一个类:VerifierAllCapsCheckerLetterBChecker
    2. 相应地重构您的测试类 - 现在应该有三个测试类。
    3. 使用您最喜欢的 DI 方法将两个内部逻辑类注入 Verifier
    4. VerifierTests 类应安排两个依赖项并将其注入Verifier,并且仅测试Verifier 逻辑(在本例中仅测试逻辑运算符)。

    在这里你可以找到 VerifierVerifierTests 类的改编版本,只是为了了解这个想法(我在这里使用了 Moq):

    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"));
            }
        }
    }
    

    【讨论】:

    • 这似乎表明一个类应该只有一个方法——对吗?
    • 我想说,一般来说,一个类中应该只有一个 public 方法(参见SRP)。
    • 有趣 - 我认为我原来的示例类 Verifier 没有违反 SRP,它只做一件事(验证字符串)。此外,您建议的方法似乎导致一个类的所有内部方法被拉出多个类的单个公共方法 - 我相信这意味着我们放弃了一个类支持的内聚,也许有更好的方法?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多