2017-02-21 72 views
14

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

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

obs:我正在为已经创建的系统编写单元测试。现在重写所有架构是不可行的。
非常感谢!如何在大型复杂类中实现单元测试?

+4

您可以考虑在[softwareengineering.stackexchange.com](http://softwareengineering.stackexchange.com/)上发布您的问题,我相信它会更合适。 –

+5

我没有答案可以添加到已经在这里的好的,但只是提出一点 - 这是TDD中的“设计”D.当某些东西很难测试时,它会告诉你代码的设计有问题。 –

+1

@VincentSavard在引用其他网站时,指出[交叉发布令人不悦](http://meta.stackexchange.com/tags/cross-posting/info) – gnat

回答

11

如果这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


注意:如果全部特性相关的特性,它正在测试,他们应该有特定的值,那么就没有图书馆,这将猜测值您的测试需要。唯一的方法是手动指定测试值。尽管如果您在每次测试之前设置一些默认值,并且只需更改特定测试的需要,就可以省去很多工作。即创建的helper方法(S),这将创造与事先定义的值对象:

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

然后在您的测试只是覆盖一些领域:

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

嘿谢尔盖。首先感谢您的答复。不幸的是,我确实需要填充所有这些对象才能测试该方法。为了测试方法计算结果,我需要有有效的条目来产生已知的结果。所以我不能使用随机输入。 NBuilder会帮助我吗? –

+3

@Lisa如果你需要确切的值,任何一个库会猜测你需要哪些值?你应该手动提供这些值 –

+0

这正是我的观点。我想知道是否手动填充超过100个属性是填充此对象的唯一方法。坦率地说,我甚至在寻找一些可以接收对象的库,并为我生成“属性人口”。当你要求sql中的某个表生成当前内容的数据插入脚本时类似。 –

1

第一件事,第一 - 你应该做的通过接口完成此对象的获取,如果代码目前正在从数据库中取出。然后,您可以嘲笑该接口,以便在单元测试中返回任何您想要的内容。

如果我在你的鞋子里,我会提取实际的计算逻辑,并写出测试用于新的“计算器”类。尽可能分解所有事物。如果输入有100个属性,但并非所有属性都与每次计算相关 - 请使用接口将其拆分。这将使预期的输入可见,并改善代码。

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

public class BigClass : ISet1 
    { 
     public string Prop1 { get; set; } 
     public string Prop2 { get; set; } 
     public string Prop3 { get; set; } 
    } 

    public interface ISet1 
    { 
     string Prop1 { get; set; } 

     string Prop2 { get; set; } 
    } 

    public interface ICalculator 
    { 
     CalculationResult Calculate(ISet1 input) 
    } 
+2

谢谢,斯蒂芬。但我的问题不是如何创建对象,而是如何填充它。在你的例子中,我的类“BigClass”有超过100个属性,可以是int,字符串,列表和其他类。我的问题是,我怎样才能优化这个对象的创建,因为我需要让所有的对象都有已知的值来生成有效的结果。 –

+1

我想说,你不应该看看如何用值填充对象,你显然不关心特定的计算。相反,你应该尝试“提取”你想测试的逻辑。如果您尝试测试整个集合,无论您的初始班级在做什么,即使您手动设置了所有100个字段,测试也无法维护。 我的建议是提取一个“计算器”,并确保它只需要它所需的值。在我的例子中,这是ISet。在你的测试中,你创建一个BigClass的实例并将其转换为ISet,只设置你关心的属性。 –

4

鉴于限制(坏代码设计和技术债务...我孩子)单元测试将是非常繁琐的手动填充。混合集成测试将需要您必须击中实际数据源(而不是生产环境)。

潜在药​​水

  1. 创建数据库的副本,并仅填充填充依赖复杂的类所需要的表/数据。希望代码足够模块化,数据访问应该能够获取和填充复杂的类。

  2. 模拟数据访问,并将它通过备用源导入必要的数据(平面文件也许?CSV)

所有其它代码可以集中在嘲笑执行单元所需的任何其他依赖测试。

除了剩下的唯一其他选项是手动填充类。

另一方面,它的代码味道很糟糕,但由于目前无法更改,所以超出了OP的范围。我建议你向决策者提一提。

4

对于我必须获得大量实际正确数据进行测试的情况,我已将数据序列化为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

0

使用的内存数据库的单元测试

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

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

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

如果这种方法对您有用,这里有一些链接可以帮助您。

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(“FullUri = fi ?文件::存储器:高速缓存=共享“);

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

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

1

我会采取这种做法:

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

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

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

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

5 - 一切都以绿色显示,您可以删除全部/部分多余的原始集成测试 - 只要您感觉舒适,就可以自行删除。

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

相关问题