Why test POJOs?

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.

And they are often untested.

The argument against testing them is usually based on the assumption that POJOs have state but no behavior. A POJO is usually just a bag of data with getters and sometimes setters. If you’ve ever seen a unit test for the typical POJO getter—the kind that directly returns a variable—it seems rather pointless. Why test that the language does what the language does?

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).

And that’s certainly a fair point. I’d wager that the majority of people actually avoid testing POJOs for another reason (because its tedious), but I guess it’s nice to know that there’s at least one solid argument to support developer laziness.

I’d like to examine the question from another context, though, one which I think changes everything: object-oriented design.

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:

All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation

In Java terms, the UAP is asking, “when you call a getter (service) on a class (module), is the value received just a piece of data or is it computed (or derived) from other data?”

Client code isn’t supposed to know.

So what? you might ask. We all know that POJOs are just some wrappers around data. Why pretend otherwise?

Well, the UAP (somewhat pedantically) points out that client code can’t make that assumption. If a module betrays its implementation to client code, or client code makes assumptions about where the value came from, it violates fundamental OO design principles.

From that lens, you should even test getters. They may simply be returning a value, but what if they’re not? Or, what if they are, but that changes in the future?

The benefit of this mindset

One goal of OO is to decouple interface from implementation, making the implementation able to evolve without forcing change on collaborating code. In the early days of Java applications, this decoupling was carried out literally by creating an interface for every class. This explicit physical separation (two separate files) underlined the idea that the current class was only one possible implementation of the interface among potentially many.

Thinking fourth-dimensionally, the “potentially many” implementations also may include the different evolutions of that same POJO over an application’s lifecycle.

So the benefit of viewing an application in the context of interfaces is that we maximize its extensibility. With interfaces as the units of application design, we can evolve any given part of the application that needs to change without imposing those changes to any collaborating classes. They are decoupled from the implementation. The interface acts as a fence around change. As long as the interface doesn’t change, collaborators don’t need to either.

In practical terms, that means we can “move fast” in reaction to changes in user demands and/or the business environment.

The purpose of tests

OK, how is that relevant to testing? Well, moving fast is possible only when developers can quickly and safely evolve implementations. So I believe that, along with the compiler, tests should provide that safety. Tests should guarantee that implementations adhere to the interface they advertise.

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

This is not just theoretical. It comes up all the time. Here are some examples.

Agreement between constructor or setters and getters

Even though a POJO is a “dumb object” it is still expected that after construction an object’s getters return the value the object was instantiated with. And if it’s a mutable object (which you should avoid but that’s probably a conversation for another time) then after you call a setter, a call to its corresponding getter should return the new value.

The fact that so many POJOs or POJO parts are created by copy-and-paste makes this not so much of a sure thing. It’s pretty easy for two getters, let’s say getFirstName and getLastName to get fat-fingered by fallible humans as follows:

public String getFirstName() {
  return firstName;
}

public String getLastName() {
  return firstName; // ack!
}

Enter fullscreen mode Exit fullscreen mode

Will the compiler catch that? Nope. Will a hurried developer? Sometimes? Will a good unit test? Yes.

Insuring the meaning of data

Even “dumb” data can have a contract. I alluded to this earlier when I asked, “if this is an object, what is the meaning of null?” There are all kinds of situations where data has special values like null, -1, a value from an enum, or something else. That’s a whole lot of situations where tests should really help give us assurances that our application is working correctly. Another class of data-only problems arises out agreement between multiple data fields. For example, if I worked zero hours in a pay period, I can’t still have worked overtime. Similarly, if there’s any kind of validation in a given POJO, that should be tested. What if a getter returns an int but that int represents a percentage? Is it okay if that number is ever less than zero or greater than 100? How will that application’s tests insure against developer mistakes?

And, by the way, a percentage returned from a getter is a great example where the UAP fully applies. A percentage is probably computed from two other values, but who knows?

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

The following explanation of feature envy is given:

The calculateWeeklyPay method reaches into the HourlyEmployee object to get the data on which it operates. The calculateWeeklyPay method envies the scope of HourlyEmployee. It “wishes” that it could be inside HourlyEmployee.

Of course, the answer is to move calculateWeeklyPay into HourlyEmployee. Unfortunately, that is a problem if you have a codebase with a rule excluding HourlyEmployee‘s package from testing and code coverage.

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?

I hope this at least shows the most ardent POJO-testing opponent that there are good reasons for testing them. Perhaps now you’ll at least add tests if any of these cases arise in your existing POJOS. What do you think? If you don’t test POJOs right now, do you think you will start? Or do you test them already? If so, why? What are your reasons? Let me know your thoughts in the comments below.

原文链接:Why test POJOs?

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容