3

我正在尝试测试我的课程

public class Parser
{

    private static IDictionary<String, Regex> PhrasesToRegexp { get; set; }

    public static void InitPhrases(IList<String> Phrases, Boolean useDeclination )
    {
        throw new NotImplementedException();
    }

    ...

    public ParsingResults Find(String source)
    {
        HtmlDocument doc = new HtmlDocument();
        doc.LoadHtml(source);
        return new ParsingResults(FindUrls(doc), CountPhrases(doc));
    }



    private IList<String> FindUrls(HtmlDocument source)
    {
        return source.DocumentNode.SelectNodes("//a[@href]").
            Select(link => link.GetAttributeValue("href", "")).ToList();
    }

    private IDictionary<String, int> CountPhrases(HtmlDocument source)
    {
        IDictionary<String, int> results = new Dictionary<String, int>();
        foreach (String key in PhrasesToRegexp.Keys)
        {
            results.Add( key , 0 );
        }

        foreach (HtmlNode node in source.DocumentNode.SelectNodes("//p"))
        {
            foreach (String phrase in results.Keys)
            {
                results[phrase] += PhrasesToRegexp[phrase].Matches
                    (Regex.Replace(node.InnerText, @"<(.|\n)*?>", string.Empty)).Count;
            }
        }
        return results;
    }

}

问题是属性PhrasesToRegexp已(将)初始化InitPhrases,我正在尝试为方法 Find 编写单元测试。基本上我需要设置这个私有属性的值PhrasesToRegexp。有没有办法做到这一点?我不是模拟专家,但我认为他们不会这样做,因为这个属性和测试方法在同一个对象中。

4

5 回答 5

1

您可以添加一个专门用于单元测试的新构造函数,但我建议尽量减少对您的类的任何更改以使其可单元测试。支持单元测试的专业化通常意味着您没有测试将在最终应用程序中运行的实际代码。你对它的专业化程度越高,就越难确保真正的代码得到了全面的测试,并且更有可能将不需要的副作用引入到被测代码中。

相反,(如果可能的话)我尝试像客户端一样使用该类 - 如果您构建实例并像客户端那样调用该方法,那么您不需要在私有状态下闲逛,并且您的单元测试将准确测试任何客户端代码将使用什么。此外,如果您更改类的内部工作方式,您的测试更有可能保持有效/有效,因为单元测试没有特殊的途径,您可以忘记与代码更改保持同步。

如果您更喜欢公开属性并直接刺激它,那么将其更改为internal并使用InternalsVisibleTo是一种标准方法,但这会冒犯我的封装感,因为它对每个人来说都是永久性的。另一个程序员怎么知道你的意思是“用于测试的内部”而不是“嘿,我们是好朋友,请尽情享受我的内部状态”。如果我们在想要进行单元测试时将其丢弃,那么私有有什么用?因此,另一种保持代码私有的方法是使用特殊的构建进行单元测试,它设置了一个#define 以允许您公开您想要访问以进行测试的私有内容,同时在您的正常构建中将它们保留为私有。

一种方法是对属性本身施加蛮力(但这可能非常混乱):

#if UNIT_TEST
    public
#else
    private
#endif
int MyPrivateProperty { get; set; }

或者,一种更简洁的方法(但更多的工作)是保留原始代码并添加访问方法,以最大程度地减少您无意中破坏/更改被测代码的机会。

private int MyProperty { get; set; }

#if UNIT_TEST
    public int AccessMyProperty
    {
        get { return(MyProperty); }
        set { MyProperty = value; }
    }
#endif 
于 2013-01-06T23:41:34.410 回答
1

在我的单元测试项目中,我创建了一个扩展原始类的 MockClass,没有重写任何方法或属性。

  • 原始类:私有的属性被提升为受保护的。
  • 模拟类:创建了一个设置受保护属性的公共方法。

使用这种方法,您不必将可见性更改为内部,这比受保护的要明显得多。当然,这也意味着如果开发人员想要访问您的属性,他可以通过扩展您的类来访问您的属性,就像您的单元测试一样。

public class Parser
{
      protected static IDictionary<String, Regex> PhrasesToRegexp { get; set; }
      ...
}

public class MockParser : Parser
{
     public MockParser() : base()
     {
     }

     public void AddPhraseToRegexp(String key, Regex value)
     {
         // Add it
         PhrasesToRegexp.Add(key, value);
     }

     public void CreatePhrasesToRegexp(IDictionary<String, Regex> newDict)
     {
         // Create a new Dictionary
         PhrasesToRegexp = newDict;
     }
}

注意:这不适用于静态或密封类

于 2016-02-17T11:02:26.880 回答
0

这可能取决于您使用的模拟框架。我通常使用 Moq 或 NMock2,在这些情况下,可行的选择是:

1)使属性内部而不是私有,这允许您在单元测试中设置它。如果您的单元测试在单独的项目中,您可能必须使用 InternalsVisibleTo 属性。

2) 为将接受 PhrasesToRegexp 的单元测试目的创建一个单独的构造函数

于 2013-01-06T22:58:25.920 回答
0

1)而不是private您应该将其声明为internal.

2)在 AssemblyInfo.cs 你应该添加程序集,从那里你的类将被访问如下

[assembly: InternalsVisibleTo("MainFormUnitTests")]
于 2013-01-06T22:59:34.653 回答
0

对我来说,之所以会出现这个问题,是因为您在解析器中表达了两种不同的关注点。

  1. 短语和正则表达式的快速参考缓存
  2. 解析逻辑

我建议将这两个问题分成两个不同的类别。此外,您可能希望在某些时候清除静态字典的内存(或使缓存无效)。

如果您在接口后面取出缓存逻辑,ICachePhrasesAndRegex那么您可以轻松地模拟依赖项以进行测试。

于 2013-01-06T23:26:14.490 回答