Photo by Louis Reed on Unsplash
POJOs (Plain Old Java Objects) and their ilk such as value objects and JavaBeans occupy a unique place in modern Java applications. They have hundreds of slightly different use cases. They are often serialized and deserialized into JSON, XML, or SQL. They form the bridges between layers in complex applications. They represent the domain specific language.
而且它们经常未经测试。
反对测试它们的论点通常基于POJO具有状态但没有行为的假设。 POJO通常只是带有getter和setter方法的数据包。 如果您曾经见过针对典型POJO getter(直接返回变量的类型)的单元测试,那似乎毫无意义。 为什么要测试该语言的功能?
The idea of testing such a getter doesn't pass even the simplest test of the most ardent test-driven advocate. In a Stack Overflow answer, Robert C. Martin says:
My personal rule is that I'll write a test for any function that makes a decision, or makes more than a trivial calculation. I won't write a test for i+1, but I probably will for if (i<0)... and definitely will for (-b + Math.sqrt(b*b - 4*a*c))/(2*a).
这当然是一个公平的观点。 我敢打赌大多数人其实避免出于其他原因测试POJO(因为它很乏味),但我想很高兴知道至少有一个可靠的论点支持开发人员的懒惰。
不过,我想从另一个上下文来研究这个问题,我认为这会改变一切:面向对象的设计。
Decoupling and The Uniform Access Principle
Let's start with the Uniform Access Principle (UAP). Largely forgotten in today's world, the UAP states that:
模块提供的所有服务应通过统一的符号表示,无论是通过存储还是通过计算实现,都不应背叛
用Java术语来说,UAP会问:“当您在类(模块)上调用getter(服务)时,接收到的值仅仅是一条数据还是从其他数据计算(或派生)?”
客户端代码不应该知道。
所以呢?你可能会问。我们都知道POJO只是数据的包装。 为什么要假装呢?
好吧,UAP(有点古怪地)指出,客户端代码无法做出这种假设。 如果模块将其实现背叛于客户端代码,或者客户端代码对值的来源进行了假设,则它违反了基本的OO设计原则。
从这个角度来看,您甚至应该测试吸气剂。 他们可能只是在返回一个值,但是如果他们不返回该怎么办? 或者,如果是的话,那将来会改变吗?
The benefit of this mindset
OO的一个目标是使接口与实现脱钩,使实现能够在不强制更改协作代码的情况下发展。 在Java应用程序的早期,这种解耦实际上是通过为每个类创建一个接口来实现的。 这种明显的物理分离(两个单独的文件)强调了这样一个想法,即当前类只是潜在的许多接口中的一种可能的实现。
第四维考虑,“潜在的许多”实现还可能包括在应用程序生命周期中同一POJO的不同演变。
因此,在接口的上下文中查看应用程序的好处是我们可以最大化其可扩展性。 使用接口作为应用程序设计的单元,我们可以演化应用程序中需要更改的任何给定部分,而无需将这些更改强加给任何协作类。 它们与实现分离。 该界面充当变更的围栏。 只要接口不会改变,协作者也不需要。
实际上,这意味着我们可以“快速行动”以响应用户需求和/或业务环境的变化。
The purpose of tests
好的,这与测试有什么关系? 好吧,只有当开发人员可以快速安全地发展实现时,才能快速行动。 因此,我认为测试应与编译器一起提供这种安全性。 测试应确保实现遵循其发布的接口。
When I say "interface" in this context I'm not talking only about Java's interface keyword, but the OO term. Do the methods exposed by a class behave as the client expects? If the return type is an object, what does null mean? If a primitive, are there certain values like -1 which indicate that something doesn't exist? What validation does or doesn't happen to input that's passed in? What is required from that input?
Basically, think of tests as one way to insure method contracts. In modern Java, tests are the best way to do that. Tests provide a verification of the method contract that the compiler cannot. We don't want to test that the language does what the language does (the compiler already does that), but there are still other things we need to know.
Practical examples
这不只是理论上的。 它总是出现。 这里有些例子。
Agreement between constructor or setters and getters
即使POJO是“哑对象”,仍然可以预期在构造后,对象的吸气剂将返回实例化该对象的值。 如果它是一个可变对象(应避免使用,但可能是另一次对话),则在调用setter之后,对其对应的getter的调用应返回新值。
通过复制和粘贴创建了如此多的POJO或POJO零件这一事实使得这并不是很确定的事情。 假设两个吸气剂很简单getFirstName和getLastName使易犯错误的人变得肥胖,如下所示:
public String getFirstName() {
return firstName;
}
public String getLastName() {
return firstName; // ack!
}
编译器会捕捉到吗? 不。 会赶时间的开发商吗? 有时? 一个好的单元测试会吗? 是。
Insuring the meaning of data
甚至“哑”数据也可以签订合同。 当我问“如果这是一个对象,null的含义是什么?”时,我曾提到过这一点。 在各种情况下,数据都有特殊值,例如null,-1,枚举值或其他。 在很多情况下,测试应该真正帮助我们确保应用程序正常运行。 另一类仅数据问题产生于多个数据字段之间的一致性。 例如,如果我在工资期内工作零小时,那么我仍然不能加班。 同样,如果给定的POJO中存在任何类型的验证,则应进行测试。 如果一个吸气剂返回一个int但那个int代表一个百分比怎么办? 如果该数字小于零或大于100可以吗? 该应用程序的测试将如何确保不会出现开发人员错误?
顺便说一句,从吸气剂返回的百分比是UAP完全适用的一个很好的例子。 一个百分比可能是计算的从另外两个值中得出,但是谁知道呢?
Adding behavior where there once was none
Perhaps you're philosophically opposed to POJOs having behavior, but in a real life application it is sometimes better than the alternative. A classic example is refactoring to eliminate feature envy. Feature Envy is a famous code smell introduced by Martin Fowler in the book Refactoring and it is also seen in the classic book Clean Code by Robert C. Martin, where it appears as G14 in the Smells and Heuristics chapter. There, the following example is given:
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
int overtimePay = (int)Math.round(overtime*tenthRate*1.5);
return new Money(straightPay + overtimePay);
}
}
以下是对功能嫉妒的解释:
的计算周薪方法进入每小时员工 object to get the data on which it operates。 的计算周薪方法羡慕范围每小时员工。 “希望”它可能在里面每小时员工。
当然,答案是移动计算周薪进入每小时员工。 不幸的是,如果您的代码库中的规则不包含每小时员工的软件包来自测试和代码覆盖范围。
Equality and identity
Final quick example. One of the main uses of POJOs is to give the application meaningful equality logic. But uses cases differ; do you want to check equality or identity? The main thing that defines a value object is that it is equal to another value object when their respective members are all equal. But other types of POJOs probably need to be differentiated based on identity. Is it always obvious which are which? Do you trust your dev team (or yourself) to automatically know the difference and add or remove tests as things change. Should you provide tests to guarantee that behavior?
What do you think?
我希望这至少表明最热心的POJO测试对手有充分的理由进行测试。 也许现在你至少加测试您现有的POJOS中是否有这些情况。 你怎么看? 如果您现在不测试POJO,您是否认为会开始? 还是已经测试过它们? 如果是这样,为什么? 你是什么原因 在下面的评论中让我知道您的想法。