loading...
Cover image for Why test POJOs?

Why test POJOs?

scottshipp profile image scottshipp ・7 min read

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!
}

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

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.

Discussion

pic
Editor guide
Collapse
mt3o profile image
mt3o

You are insisting on testing a thing, that might belong to someone else, right? (client code assumption) well, you should test YOUR code, not 3rd party.
You stated that there might be additional logic in getter/setter. Or that there might be a bug in getter. This is serious claim, specifically in times when getters and setters are generated by IDE or directly the compiler (thanks to lombok).
I attempted to test getters and setters once. I did it with reflection, scanned for all of them and tested for primitives and boxed primitives. Goal was to drastically increase coverage stats and it proved to my coworker that there was no point for it. After that team agreed to remove POJO from coverage stats.

Collapse
franzsee profile image
Franz Allan Valencia See

I would argue not removing POJO for test coverage stats. Because it may flesh out unused getters or setters that can be removed.

Even if let’s say that an accessor method is never directly referenced by your code, and is only access through some marshalling/unmarshalling of JSON or DB Rows, then that may just mean that you dont have enough test on the marshalling/unmarshalling logic

Collapse
mt3o profile image
mt3o

Focus. It's not about that it's referenced by marshalling, or reflection in general, but about not being a part of direct code base. They are generated by the compiler.

There are reasons for providing 70%-80% on code coverage, and agreement on not testing POJO is one of them.

I think that in general you have to rethink WHY people write tests, or even TDD, and why they they don't.
Also, what are the drawbacks of having 100% coverage.
Please note that coverage doesn't guarantee that your code will be bug free, and instead of focusing on scoring coolness points, focus on bugs coming from misuse of programs logic, faulty incoming data and concurrency.

Collapse
nickhristov profile image
Nick Hristov

Nope.

Either you should have your ide generate your pojo methods, or use lombok. Or use kotlin.

In all cases testing pojos is stupid. Unless you want to test serialization or deserialization but then you are not just testing pojos.

The blind religion of "everything must be tested" is irrelevant in the professional world where developer time is money, and test code must produce sufficient value: if it doesn't, then you are wasting your time. And I also mean to imply that not all tests are created equal not all are equally valuable.

Collapse
franzsee profile image
Franz Allan Valencia See

Tests are meant to ensure behavior. Not data structure. However, most behaviour would have to work with some data structures.

One thing I learnt from TDD is that your POJOs test coverage are automatically covered by the logic that’s working against it.

(Gasp! That’s a design smell on itself which is why I hate POJOs. They pretend to be objects wherein in fact, they’re just data structures. Both OOP and Procedural practice loose coupling and tight cohesion principally. The main difference is what they consider to be tightly coupled. In procedural, data and behaviour are separated. That’s because there is no one owner of the data. In OOP, the data and the behaviour that acts on it needs to be in one Object. That way, you enforce abstraction and encapsulation because the data is just part of the implementation details.)

Collapse
aminmansuri profile image
hidden_dude

Are we using terminology correctly here?

I always understood "POJO" to mean "Plain old java object" and first saw it with the introduction of Spring where Services were just "POJOs" ie. they didn't inherit from some weird framework but used inversion of control to get other services injected. So POJO to me meant just plain old java objects without weird framework dependencies. (en.wikipedia.org/wiki/Plain_old_Ja...)

What is being discussed here is a "Value object" an object that just has getters/setters that stores data, and in that sense is nothing more than a glorified struct. (en.wikipedia.org/wiki/Value_object)

Am I getting my terminology wrong?

Collapse
moaxcp profile image
John Mercier

I always thought of value objects as immutable (no setters). A POJO can have setters. I think lombok's @Data and @Value provide good standards.

Thread Thread
aminmansuri profile image
hidden_dude

It may be a good practice for a value object to be immutable but it's not required.

POJO just means any Java object that doesn't depend on a weird framework.

Collapse
scottshipp profile image
scottshipp Author

It's more that people commonly misuse the term POJO so that in common slang usage it really could mean any of POJO, JavaBean, value object, or more.

Collapse
mortoray profile image
edA‑qa mort‑ora‑y

Testing is about time optimization: you'll never have enough time to test everything, thus you spend your time on the highest value tests.

POJOs will never make it high enough in the priority list to bother testing directly. Because they are always part of some other use-case, the test for those use-cases transitively include the tests for the POJOs. In most projects, adding extra tests for POJOs provide no extra coverage, nor increase in quality -- errors in them will show up in the other tests.

Collapse
scottshipp profile image
scottshipp Author

Never say never 🙂

Collapse
chaotic3quilibrium profile image
Jim O'Flaherty

While I like your argument from principle, I still think it's missing a critical point. There's an even higher set of principles which can be applied which integrate several higher values and motivations such that it easily resolves the many explicit and implicit conflicts which appear within your post.

An implied conflict you're trying to resolve within your article is between time spent assuming a software component is sufficiently trustworthy versus demonstrating it's actually trustworthy with an (automatable) battery of tests. This same conflict occurs even more strongly in dynamically typed languages (like JavaScript and Python). And the ambiguity you point out is resolved by attempting to identify a threshold at which you ought test a POJO getter (and possibly also its setter mate[s]).

So, if we step back and talk about a couple of higher values, some of which you mention in passing, we can start to evaluate and appreciate other possibly more effective solution pathways. Pathways which clearly and succinctly identify precisely when, where, and how much to write the tests.

The first higher value is "willpower". It is the Software Engineer's (including any developer, programmer, data scientist, etc.) HIGHEST and MOST SCARCE value. It is also THE system constraint. And as we know from ToC (the Theory of Constraints by Eliyahu Goldratt), this means we must strive to subordinate ALL activities such that they maximize the value of the time spent on THE system constraint.

Willpower?!? Yes. It is the EXTREMELY SCARCE SINGLE THREADED resource utilized to evaluate and then produce both the POJO and the accompanying automated tests. So, every second ineffectively and/or inefficiently utilized, or worse, generates irritation, frustration, and/or resistance, is a precious second not being spent on other higher value activities. That second is gone forever. And, it is quite literally the basis of the reason you observe the seemingly irrational feuds about code formatting, the "best" code editor, the proper way to use "null, etc.. And that leads us into the second higher value...

Writing code for fast readability later to facilitate EASIER REASONING AND REFACTORING is the highest goal, and not for other much lower priority concerns (like fewer characters, fewer lines, fewer classes, some probably irrelevant execution optimization, some seemingly clever trick, etc.). As you mention in your article, the code is read many MANY more times than it is ever written. It follows, then, that we focus on optimizing the primary constraint (willpower) of coders that follow us later. And that is their reading AND RAPID UNDERSTANDING of said code

Once you understand and appreciate why these two values ought to be amongst the highest values when evaluating which software engineering principles to use, the POJO testing discussion magically transforms into a whole different exploration. And then finding the "where to test threshold" becomes an easy call. It also vastly reduces the resistance to producing needed POJOs because the testing burden and maintenance is substantially reduced (if not eliminated.

So, how do we translate that into actual thinking and action? What are some examples which are derived from and illuminate the effective use of these new higher value distinctions?

Oops, I've run out of time. If you're at all curious, hit me up (jim dot oflaherty dot jr at gmail dot com) and let me know. I'd be happy to write you back about it. Even here as another comment, if you like. It's too much work to write more without knowing it is anticipated.

BTW, I'm deeply passionate about software engineering, psychology, and ToC, just in case that did't come through loud enough, LOL!

Collapse
jorgecc profile image
Jorge Castro

tsk tsk.

https://media1.giphy.com/media/UWqS4E1O7Kes0/giphy.gif

A POJO class is not exclusively a Model class. A Model class could be a POJO but also a Service and a (rich) Business Object class.

Also, most Java developers use Lombok.

dev.to/code2bits/how-to-generate-j...

Collapse
aminmansuri profile image
hidden_dude

they are confusing "value object" with POJO it seems.

Collapse
sakfa profile image
sakfa

"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?"

I don't really get the "what if" part - you write tests to cover implementation of your code, there's no "what if", you know what your getter does because you just wrote it!

Writing a test for a getter just because it might, in the future, change implementation to ad-hoc computation looks to me like an example of violation of YAGNI (you ain't gonna need it) and future coding - yes, everything might change logic in the future. But I bet that 99 (or even 99.9%) of getters you wrote through your career never did. And those that did probably changed so much that the test you would have wrote in the past would became obsolete and completely rewritten anyway.

Also nothing in software happens by accident. In the future the implementation will change if and only if someone comes to that getter code and makes a change. If that changes implementation from trivial to non-trivial it's that someone who should write a test, not some random developer who generated trivial POJO 5 years ago.

Collapse
scottshipp profile image
scottshipp Author

If I read you correctly, your logic is that since you know what your getter does you don't need to test it. That's the same logic that people use to not test any of their code. "I know what the code does. I don't need to test it."

Tests prove that your code does what you think it does, though.

Another question I have is can you guarantee that if anyone else is going to touch that code they'll write a test for it? In a shared codebase, that's a nice idea, but not the practical outcome usually. Especially if there's no code coverage requirement on that package, so you'll see people neglect adding any tests there.

Collapse
sakfa profile image
sakfa

I get your point about tests prove your code does what you think it does, but as always common sense applies - you already mentioned one of the arguments I would make here "Why test that the language does what the language does?". I would always write a test for any method that consists of at least one actual operation, so method like getFullName() { return firstName + " " + lastName } would always warrant a assertEquals("John Smith", new Person("John", "Smith").getFullName()) test.

Also earlier you made a strong point about testing constructor contract, I obviously agree that sloppy developer could write this code:

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

where he clearly thinks his trivial code that doesn't need test does something else than what it actually does. A trivial assertEquals("Smith", new Person("John", "Smith").getLastName()) test would catch it.

But is that enough to convince me to write these tests? Not really, for 2 reasons:

  • entity classes are never mocked away meaning some other test will catch this anyway. If none of your test catches it, you probably have way more serious problems in your code base than untested POJOs so you can probably spend your precious time fixing these more serious problems first. Once you do, you no longer need POJO tests because now none of your entities have unused fields anymore and all services operating on these POJOs have at least their happy paths tested meaning all your getters and setters should either be covered or not exist.

  • any sane Java developer would generate their constructor, setters and getters for data classes. If I advocated in my team to write test to POJOs it would probably drive everyone crazy - "I just generated this 8 args constructor and 8 getters using just 2 keypresses, do you really want me now to write a test and then copy paste it 7 times with small modifications?". On the other hand if a developer doesn't generate their setters and getters he will probably spend his time way more productively learning and actually start using tools to do that, than writing these tests. Using generators will increase their productivity forever. Writing tests won't.

As for

Another question I have is can you guarantee that if anyone else is going to touch that code they'll write a test for it?

First thing to say is of course not, just as you can't guarantee that anyone else is not going to delete your tests. We can't guarantee much about behaviour of future devs, but there has to be some basic trust in organization. We shouldn't write code to protect from future misbehaving developers. What we should do though is minimize the risk of someone misbehaving by scrutinizing each change made. In my organization for instance we have following process:

  1. Any change has to have a code review published. No excuses, publishing CR is as easy as running publish-cr, if you have time to run git push you also have time to run publish-cr. So there's no reason to not do that (barring code review tool failure, perhaps)
  2. Any change should be approved by a team member or subject matter expert. This is not enforced strictly, but see next point.
  3. If a change without a CR or without an approval is pushed anyway, continuous integration approval subprocess we call "code review police" will fail to approve (and thus automatically deploy) this change.
  4. Of course we don't want to slow people down - change author (anyone actually) can still override this failure, literally saying "yes, I don't have approval. I know what I'm doing. Deploy". When he does that though, an e-mail is sent to entire team owning the package saying he did so.
  5. Team members would usually scrutinize such e-mail and review it even more thoroughly than usual (after all this code is probably already running in production). At this phase crappy code would likely be called out, but to be fair I have never witnessed anyone force-deploying crappy code. Psychology works here, if you know your "force deploy" is scrutinized you won't do this for ugly changes.

And to aid in detecting missing tests our CR tool is quite sophisticated - when change is published, it displays diff side by side, runs tests with coverage and on that side-by-side diff marks in light red lines not covered by any test. Engineers doing CR would usually raise a concern if you made a non-trivial change on a red line. Unless common sense says this line doesn't really need test, and if common sense of both author and reviewer says that - then yes, you probably do not need test.

So no, I can't guarantee future dev will write test, but with proper tooling and processes in place I can minimize the risk of having crappy code reaching production. IMHO this is way more important than any kind of artificial rules "at least 80% coverage" or "all new code has to be covered by test". To me common sense always wins against dogma. I witnessed too many changes which covered 100% lines, including all their catch blocks to reach coverage goal but failed to write a basic "Serialize/Deserialize` test ("if I serialize this object to DB and then read it back, is it equal to what I wrote?") to believe in threshold-based rules

Anyway, I understand your position and don't see anything particular wrong with it. Just wanted to share my 3 cents.

Collapse
shoff profile image
Steve Hoff

Just don't, seriously.

Collapse
atomgomba profile image
Károly Kiripolszky

Thankfully now we have data classes in Kotlin and don't have to bother with nullability annotations or accidental NPE or writing equals()/hashCode() anymore.

Collapse
seond profile image
Seon

How do you even trust your tests? Don't you need another set of tests to test the POJO tests?

Collapse
aminmansuri profile image
hidden_dude

Code coverage is a measure of test badness.. if you have low coverage you can know that your tests are probably bad. If you have high coverage you haven't found any obvious badness. But you still could be bad.

This is the same with testing in general. Testing is there to find bugs. If you don't find any bugs that doesn't mean you don't have bugs. But if you do, you've detected "badness".

Neat right?