您应该分班讨论中的课程。
每个班级应该完成一些简单的任务。如果您的任务太复杂而无法测试,则该类要做的任务太大。
忽略这种设计的愚蠢性:
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-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();
}
也许那就是你一直在做的。但是我不认为这种方法会遇到我在模拟方法中遇到的问题,因为我们已经清楚地描述了每种方法在哪一侧。通过使用继承,我们避免了使用附加包装对象时出现的尴尬。
这确实带来了一些复杂性,对于仅几个实用程序功能,我可能只是通过模拟底层的第三方来源对其进行测试。当然,它们处于断裂的危险越来越大,但不值得重新布置。如果您有足够复杂的对象需要拆分,那么我认为类似的想法是个好主意。