Today, after a month of working on a new feature, we were ready to have a bug bash, which essentially saved me before ramping this new feature to the public.
As I began debugging my code, I found myself examining a piece of code that utilizes streams, a crucial tool for functional programming in Java when working with collections.
I appreciate how elegant, concise, and readable the code becomes when using map to transform items, apply filters, find minimum and maximum values. However, this time, my lack of experience using these features worked against me.
So, What happened?
I was working with a piece of code performing some transformations like this:
entities.stream()
.map(Entity::getId)
.map(new EntityCriteria()::setEntity)
.collect(Collectors.toList())
If you're more experienced than I was when I wrote this code, you might have already spotted the issue. Allow me to explain:
-
entities
is a collection (List) and its elements are type ofEntity
-
map(Entity::getId)
will transform the elements into their IDs - The intention of map(new EntityCriteria()::setEntity) was to transform the entity IDs into EntityCriteria objects with the entityId. However, this happened, but not in the right way.
Understanding the Concepts.
To comprehend what is wrong with number 3, we need to grasp some functional programming concepts:
Entity::getId
from theEntity
class uses the::
operator to retrieve aFunction
calledgetId
. It does NOT retrieve the actual ID.
ThisFunction
will be applied to theEntity
elements to retrieve their IDs becauseEntity
is a class, andgetId
doesn't accept any arguments.When we call
map(new EntityCriteria()::setEntity)
,new EntityCriteria()
creates a new object. Then::setEntity
on that object generates aFunction
capable of applyingsetEntity
to the newEntityCriteria
object.
Root Cause
The Function
created by new EntityCriteria()::setEntity
is always applied to the same EntityCriteria
object, It doesn't create new objects and then retrieve the function to apply setEntity
.
The Most Important Question: Why this wasn't caught in Unit Tests ?
The mocked data used in the unit tests, replicated the same buggy logic for transforming the entities, so everything appeared to work.
Possible solutions:
Using Lambdas
map(entityId -> new EntityCriteria().setEntity(entityId))
When we use lambdas, we define the function where the entityId
on the left is the input to the function, and the right part is the body of the function (In this case, the transformation logic).
Creating a new method to be applied in map
private static EntityCriteria createEntityCriteria(long entityId) {
return new EntityCriteria().setEntity(entityId);
}
...
entities.stream()
.map(Entity::getId)
.map(MyClass::createEntityCriteria)
.collect(Collectors.toList())
This approach creates a Function
that applies createEntityCriteria
and that function generates a new EntityCriteria
object for each element.
Conclusion
- Everybody makes mistakes, so make sure to thoroughly test your code.
- When writing mock data in unit tests, avoid replicating the logic for building the transformation if that is what you are actually testing.
Top comments (2)
nice explanation man good job finding the bug, you can only learn from mistakes :D AIDE!!
EN ?
Thanks man! Debugger helps a lot, seriously, there is where I found out I was sending the same Object N times.