使用已测试的内部方法对公共方法进行单元测试

3

我发现自己经常陷入这种模式中,我编写了一个由非常小的方法组成的类,这些方法完全由我的单元测试覆盖。然后我发现我需要构建另一个调用这些方法的方法,我必须为此编写一个更复杂的单元测试 - 一个简单的例子会很有说明性:

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

很明显,对于这个例子来说,情况还不错,但是当有很多内部方法且输入难以配置时,测试我的公共“输入是否有效”方法就变得复杂了。
我应该为所有的内部方法创建一个接口,然后在测试中将其存根化,还是有更简洁的方式?
2个回答

4

我正在输入一条评论,但是它变得太长了。我认为你在边界上违反了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));
        }
    }

现在,您的验证器可以进行扩展,但不可修改。您可以添加任意数量的新规则,而实现不会发生改变。测试验证器就像传递一些返回任意true和false值的模拟字符串规则一样简单,并确保验证器返回适当的结果。

每个IStringRule都将被单独测试,就像您一直在做的那样。


+1,我喜欢这种方法。确实遵守了开闭原则,添加规则也将是一个低摩擦的任务。 - s.m.

0

更好的方式如下:

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

这似乎表明在单个类上应该有一个且仅有一个方法 - 这正确吗? - s d
我认为通常一个类中应该只有一个**公共(public)**方法(参见单一职责原则)。 - seldary
有趣的是,我认为我的原始示例类Verifier并没有违反SRP,它只做了一件事情(验证字符串)。 此外,你建议的方法似乎会导致一个类的所有内部方法被提取到多个类的单个公共方法中 - 我认为这意味着我们放弃了类所支持的内聚性,也许还有更好的方法? - s d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接