如何在大型和复杂的类中实现单元测试?

时间:2017-02-21 09:22:10

标签: c# unit-testing nunit moq

我正在财务系统中实施单元测试,涉及多项计算。其中一个方法是通过参数接收具有100个以上属性的对象,并根据此对象的属性计算返回值。 为了实现此方法的单元测试,我需要为所有这个对象填充有效值。

所以......问题:今天这个对象是通过数据库填充的。在我的单元测试中(我正在使用NUnit),我需要避开数据库并创建一个模拟对象,以仅测试方法的返回。如何用这个巨大的对象有效地测试这个方法?我真的需要手动填写它的所有100个属性吗? 有没有办法使用Moq自动化这个对象创建(例如)?

obs:我正在为已经创建的系统编写单元测试。目前重写所有架构是不可行的。
万分感谢!

6 个答案:

答案 0 :(得分:13)

如果这100个值不相关且您只需要其中的一些,那么您有几个选项。

您可以创建新对象(属性将使用默认值初始化,例如字符串为null,整数为0)并仅分配必需的属性:

 var obj = new HugeObject();
 obj.Foo = 42;
 obj.Bar = "banana";

你也可以使用像AutoFixture这样的库来为你的对象中的所有属性分配虚拟值:

 var fixture = new Fixture();
 var obj = fixture.Create<HugeObject>();

您可以手动分配所需的属性,也可以使用夹具构建器

 var obj = fixture.Build<HugeObject>()
                  .With(o => o.Foo, 42)
                  .With(o => o.Bar, "banana")
                  .Create();

另一个用于同一目的的有用库是NBuilder

注意:如果所有属性都与您正在测试的功能相关,并且它们应具有特定值,则没有库可以猜测测试所需的值。唯一的方法是手动指定测试值。如果您在每次测试之前设置一些默认值并且只是更改特定测试所需的内容,则可以省去大量工作。即创建辅助方法,该方法将使用预定义的值集创建对象:

 private HugeObject CreateValidInvoice()
 {
     return new HugeObject {
         Foo = 42,
         Bar = "banaba",
         //...
     };
 }

然后在你的测试中覆盖一些字段:

 var obj = CreateValidInvoice();
 obj.Bar = "apple";
 // ...

答案 1 :(得分:5)

对于我必须获得大量实际正确数据进行测试的情况,我已将数据序列化为JSON并将其直接放入我的测试类中。原始数据可以从您的数据库中获取,然后序列化。像这样:

[Test]
public void MyTest()
{
    // Arrange
    var data = GetData();

    // Act
    ... test your stuff

    // Assert
    .. verify your results
}


public MyBigViewModel GetData()
{
    return JsonConvert.DeserializeObject<MyBigViewModel>(Data);
}

public const String Data = @"
{
    'SelectedOcc': [29, 26, 27, 2,  1,  28],
    'PossibleOcc': null,
    'SelectedCat': [6,  2,  5,  7,  4,  1,  3,  8],
    'PossibleCat': null,
    'ModelName': 'c',
    'ColumnsHeader': 'Header',
    'RowsHeader': 'Rows'
    // etc. etc.
}";

当您进行大量此类测试时,这可能不是最佳选择,因为以这种格式获取数据需要相当多的时间。但是,这可以为您提供一个基本数据,您可以在完成序列化后为不同的测试进行修改。

要获得此JSON,您必须单独查询数据库以查找此大对象,通过JsonConvert.Serialise将其序列化为JSON并将此字符串记录到源代码中 - 这一点相对简单,但需要一些时间因为你需要手动完成......但只需要一次。

当我必须测试报告呈现并且从数据库获取数据时,我已经成功地使用了这种技术。

P.S。您需要Newtonsoft.Json个包才能使用JsonConvert.DeserializeObject

答案 2 :(得分:4)

考虑到这些限制(糟糕的代码设计和技术债务......我生小孩)手动填充单元测试非常麻烦。如果您需要访问实际数据源(而不是生产中的数据源),则需要进行混合集成测试。

潜在的魔药

  1. 制作数据库的副本,并仅填充填充依赖复杂类所需的表/数据。希望代码模块化足以使数据访问能够获取并填充复杂类。

  2. 模拟数据访问并让它通过备用源导入必要的数据(平面文件可能是?csv)

  3. 所有其他代码都可以专注于模拟执行单元测试所需的任何其他依赖项。

    除了剩下的唯一选择是手动填充类。

    顺便说一句,这有不好的代码味道,但这超出了OP的范围,因为此时无法更改。我建议你向决策者提及。

答案 3 :(得分:1)

首先,首先 - 如果当前的代码从数据库中提取,则应通过接口完成此对象的获取。然后,您可以模拟该接口,以便在单元测试中返回您想要的任何内容。

如果我参与其中,我会提取实际的计算逻辑,并将测试写入新的“计算器”类。尽可能地分解一切。如果输入具有100个属性,但并非所有属性都与每个计算相关 - 请使用 interfaces 将它们拆分。这将使预期输入可见,同时改进代码。

所以在你的情况下,如果你的类让我们说名为BigClass,你可以创建一个将在某个计算中使用的接口。这样您就不会更改现有类或其他代码使用它的方式。提取的计算器逻辑将是独立的,可测试的和代码 - 更简单。

public class WhenCreating : GivenConnection
    {
        [Fact(DisplayName = nameof(GivenConnection) + "." + nameof(WhenCreating ) + "." + nameof(ItAppliesFromCity))]
        public void ItAppliesFromCity()

答案 4 :(得分:1)

我采取这种方法:

1 - 为100属性输入参数对象的每个组合编写单元测试,利用工具为您执行此操作(例如pex,intellitest)并确保它们都运行为绿色。此时,将单元测试称为集成测试而不是单元测试,原因后来会变得明显。

2 - 将测试重构为SOLID代码块 - 不调用其他方法的方法可以被认为是真正的单元可测试的,因为它们不依赖于其他代码。其余的方法仍然只能集成测试。

3 - 确保所有集成测试仍在运行绿色。

4 - 为新的单元可测试代码创建新的单元测试。

5 - 当所有内容都运行为绿色时,您可以删除所有/部分多余的原始集成测试 - 仅限于您,只有在您觉得这样做时才会这样做。

6 - 当一切都运行为绿色时,您可以开始将单元测试中所需的100个属性减少到只有每个方法严格需要的属性。这可能会突出显示额外重构的区域,但无论如何都会简化参数对象。反过来,这将使未来的代码维护者工作减少错误,并且当有50个属性时,我打赌历史性的失败来解决参数对象的大小,这就是为什么它现在是100.现在没有解决问题将意味着它'最终将增长到150个参数,让我们面对它,没人想要。

答案 5 :(得分:0)

使用内存数据库进行单元测试

所以......从技术上说,这不是你所说的单元测试的答案,而使用内存数据库使其成为集成测试,而不是单元测试。但是,我发现有时候面对不可能的约束时,你需要给某个地方,这可能就是其中之一。

我的建议是在单元测试中使用SQLite(或类似的)。有一些工具可以将您的实际数据库提取并复制到SQLite数据库中,然后您可以生成脚本并将其加载到内存版本的数据库中。您可以使用依赖项注入和存储库模式在您的&#34; unit&#34;中设置数据库提供程序。测试比实际代码。

通过这种方式,您可以使用现有数据,在需要时作为测试的前提条件进行修改。您将需要确认这不是真正的单元测试...这意味着您仅限于数据库可以真正生成的内容(即表约束将阻止测试某些方案),因此您无法在这种意义上进行完整的单元测试。此外,这些测试运行速度较慢,因为它们确实在进行数据库工作,因此您需要计划运行这些测试所需的额外时间。 (虽然他们通常仍然很快。)请注意,你可以模拟任何其他实体(例如,如果除了数据库之外还有服务电话,那仍然是模拟潜力)。

如果这种方法对您有用,可以使用以下链接来帮助您。

SQL Server到SQLite转换器:

https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-to-SQLite-DB

SQLite工作室: https://sqlitestudio.pl/index.rvt

(用于生成脚本以供内存使用)

要在内存中使用,请执行以下操作:

TestConnection = new SQLiteConnection(&#34; FullUri = file :: memory:?cache = shared&#34;);

我从数据加载中有一个单独的数据库结构脚本,但这是个人偏好。

希望有所帮助,祝你好运。