假冒被测课程的一部分可以吗?


22

假设我有一个类(请原谅人为的示例及其错误的设计):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(请注意,GetxxxRevenue()和GetxxxExpenses()方法具有依赖项,这些依赖项已被删除)

现在,我正在对取决于GetNewYorkProfit()和GetMiamiProfit()的BothCitiesProfitable()进行单元测试。可以对GetNewYorkProfit()和GetMiamiProfit()存根吗?

似乎如果我不这样做,那么我同时要同时测试GetNewYorkProfit()和GetMiamiProfit()以及BothCitiesProfitable()。我必须确保为GetxxxRevenue()和GetxxxExpenses()设置存根,以便GetxxxProfit()方法返回正确的值。

到目前为止,我只看到了对外部类而非内部方法的依赖关系进行存根的示例。

如果可以的话,是否应该使用一种特定的模式来执行此操作?

更新

我担心我们可能会遗漏核心问题,这可能是我的拙劣例子的错。基本的问题是:如果一个类中的方法依赖于该类中另一个公开的方法,是否可以(甚至建议)将另一个方法存根?

也许我错过了一些东西,但是我不确定分班总是有意义的。也许另一个更好的例子是:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

全名定义为:

public string FullName()
{
  return FirstName() + " " + LastName();
}

测试FullName()时可以对FirstName()和LastName()进行存根吗?


如果同时测试一些代码,这怎么可能不好呢?有什么问题?通常,最好是更频繁,更深入地测试代码。
用户未知,

刚刚找到有关您的更新的信息,我已经更新了我的答案
Winston Ewert

Answers:


27

您应该分班讨论中的课程。

每个班级应该完成一些简单的任务。如果您的任务太复杂而无法测试,则该类要做的任务太大。

忽略这种设计的愚蠢性:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

更新

类中的存根方法存在的问题是您违反了封装。您的测试应检查以查看对象的外部行为是否符合规范。对象内部发生的任何事情都与它无关。

FullName使用FirstName和LastName的事实是实现细节。课堂外的任何人都不应该在乎那是真的。通过模拟公共方法以测试对象,您可以假设已实现该对象。

在将来的某个时候,该假设可能不再正确。也许所有名称逻辑都将重定位到Person只是调用的Name对象。也许FullName将直接访问成员变量first_name和last_name,而不是调用FirstName和LastName。

第二个问题是为什么您觉得有必要这样做。毕竟可以测试您的人员类:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

在此示例中,您不应觉得需要存根。如果您这样做了,那么您会感到固执并且幸福...停下来!模拟那里的方法没有任何好处,因为无论如何您都可以控制它们中的内容。

对于FullName用来模拟的方法似乎有意义的唯一情况是,如果FirstName()和LastName()以某种方式是非平凡的操作。也许您正在编写这些随机名称生成器之一,或者FirstName和LastName在数据库中查询答案或其他内容。但是,如果发生这种情况,则表明该对象正在执行不属于Person类的操作。

换句话说,模拟方法是将对象分成两部分。一件被嘲弄而另一件正在测试。实际上,您正在做的是临时分解对象。如果是这种情况,只需将对象分解即可。

如果您的课程很简单,那么您就不必在测试过程中嘲笑它的各个部分。如果您的类足够复杂,以至于您需要模拟,那么您应该将该类分解为更简单的部分。

再次更新

在我看来,一个对象具有外部和内部行为。外部行为包括对其他对象等的返回值调用。显然,应该测试该类别中的任何内容。(否则您将测试什么?)但是内部行为不应真正进行测试。

现在测试内部行为,因为它是导致外部行为的原因。但是我不会直接针对内部行为编写测试,而只能通过外部行为间接编写测试。

如果我想测试某些东西,我认为应该将其移动以使其成为外部行为。这就是为什么我认为如果要模拟对象,则应该拆分对象,以便要模拟的对象现在位于所讨论对象的外部行为中。

但是,这有什么区别呢?如果FirstName()和LastName()是另一个对象的成员,是否真的改变了FullName()的问题?如果我们确定需要模拟FirstName和LastName,实际上对将它们放在另一个对象上有帮助吗?

我认为,如果您使用模拟方法,则会在对象中创建接缝。您具有诸如FirstName()和LastName()之类的函数,这些函数直接与外部数据源进行通信。您也有FullName()却没有。但是由于它们都在同一个类中,因此并不明显。有些片段不应该直接访问数据源,而另一些则可以。如果仅将这两组分开,您的代码将更加清晰。

编辑

让我们退后一步,问一下:为什么在测试时我们模拟对象?

  1. 使测试一致地运行(避免访问因运行而异的事物)
  2. 避免访问昂贵的资源(请勿访问第三方服务等)
  3. 简化被测系统
  4. 使测试所有可能的场景(例如模拟故障等)变得更加容易
  5. 避免依赖于其他代码段的细节,以使其他代码段中的更改不会破坏该测试。

现在,我认为原因1-4不适用于这种情况。测试全名时模拟外部源可解决所有这些模拟原因。唯一没有处理的就是简单性,但是似乎对象足够简单,因此不必担心。

我认为您的关注点是原因5。关注点是,将来在某些时候更改FirstName和LastName的实现会破坏测试。将来,FirstName和LastName可能会从其他位置或来源获得名称。但是FullName可能永远是FirstName() + " " + LastName()。这就是为什么要通过模拟FirstName和LastName来测试FullName的原因。

那么,您所拥有的是人员对象的某些子集,该子集比其他对象更有可能发生变化。对象的其余部分使用此子集。该子集当前使用一个源获取其数据,但在以后可能会以完全不同的方式获取该数据。但是对我来说,听起来好像那个子集是一个试图逃脱的独特对象。

在我看来,如果您模拟对象的方法,则会将对象拆分。但是您是以临时方式进行的。您的代码不清楚,Person对象中有两个不同的部分。因此,只需在您的实际代码中拆分该对象,即可轻松阅读代码以了解发生了什么。选择有意义的对象的实际拆分,不要为每个测试尝试以不同方式拆分对象。

我怀疑您可能反对拆分您的对象,但是为什么呢?

编辑

我错了。

您应该拆分对象,而不是通过模拟单个方法来引入临时拆分。但是,我过于专注于一种拆分对象的方法。但是,OO提供了多种分割对象的方法。

我的建议是:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

也许那就是你一直在做的。但是我不认为这种方法会遇到我在模拟方法中遇到的问题,因为我们已经清楚地描述了每种方法在哪一侧。通过使用继承,我们避免了使用附加包装对象时出现的尴尬。

这确实带来了一些复杂性,对于仅几个实用程序功能,我可能只是通过模拟底层的第三方来源对其进行测试。当然,它们处于断裂的危险越来越大,但不值得重新布置。如果您有足够复杂的对象需要拆分,那么我认为类似的想法是个好主意。


8
说得好。如果您想在测试中找到解决方法,则可能需要重新考虑您的设计(顺便说一句,现在我的脑袋里塞满了Frank Sinatra的声音)。
有问题的

2
“通过模拟公共方法以测试对象,您可以假设[如何]实现该对象。” 但是在您存根对象时不是这种情况吗?例如,假设我知道我的方法xyz()调用了其他对象。为了隔离测试xyz(),我必须对另一个对象进行存根测试。因此,我的测试必须了解xyz()方法的实现细节。
用户

在我的情况下,“ FirstName()”和“ LastName()”方法很简单,但是它们会向第三方api查询其结果。
用户

@User,已更新,大概是您在模拟第三方api以测试FirstName和LastName。在测试FullName时进行相同的模拟有什么问题?
温斯顿·埃韦特

@Winston,这是我的全部意思,目前,我模拟了用于firstname和lastname的第3方api以测试fullname(),但我宁愿不在意在测试fullname时如何实现firstname和lastname (当然当我测试名字和姓氏时也可以)。因此,我有关嘲笑名字和姓氏的问题。
用户

10

尽管我同意Winston Ewert的回答,但有时由于时间限制,已经在其他地方使用过的API或您所拥有的东西而无法拆分一个类。

在这种情况下,我将编写测试以逐步覆盖类,而不是模拟方法。测试getExpenses()getRevenue()方法,然后测试getProfit()方法,然后测试共享方法。的确,您会针对一个特定的方法进行多个测试,但是由于您编写了通过测试以分别覆盖这些方法,因此可以确保在经过测试的输入下,您的输出是可靠的。


1
同意 但是,只是为了强调任何阅读本书的人,如果不能做更好的解决方案,请使用此解决方案。
温斯顿·埃韦特

5
@Winston,这是您寻求更好的解决方案之前所使用的解决方案。即拥有大量的遗留代码,您必须先进行单元测试,然后才能进行重构。
彼得Török

@PéterTörök,好点。尽管我认为“无法提供更好的解决方案”已涵盖了这一点。:)
Winston Ewert

@all测试getProfit()方法将非常困难,因为getExpenses()和getRevenues()的不同方案实际上会使getProfit()所需的方案成倍增加。如果我们要独立测试getExpenses()和Revenue(),那么模拟这些方法来测试getProfit()不是一个好主意吗?
Mohd Farid

7

为了进一步简化示例,假设您正在测试C(),它取决于A()B(),每个都有自己的依赖性。IMO取决于您的测试想要达到的目标。

如果您正在测试的行为C()定的已知的行为A()B()那么它可能是最简单有效的存根出A()B()。纯粹主义者可能将此称为单元测试。

如果您正在测试整个系统的行为(从C()的角度来看),我将离开A()并按B()实现方式进行操作,或者将其依赖项存根(如果可以进行测试),或者设置沙盒环境(例如测试)数据库。我将其称为集成测试。

两种方法都可能是有效的,因此,如果将其设计为可以预先进行测试的方法,则从长远来看可能会更好。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.