2

Since recently, I am trying out unit testing to get acquainted with the practice, and to ultimately write better code. I have started on one of my big projects, with a rather large untested code base, but am only unit testing utility classes, that don't have many dependencies and are quite easy to test.

I have read quite a bit of introductory material to unit testing in general, and what often comes up is that unit tests should always test the smallest possible unit of behaviour. That is to say, that whether a test passes or fails should only depend on a very specific piece of code, such as a small method.

However, I am already finding problems to put this idea into practice. Consider for example :

@Test public void testSomethingWithFoo() { Foo foo = new Foo(5); Bar result = foo.bar(); //Suppose this returns new Bar(42) for some reason. Bar expected = new Bar(42); Assert.assertEquals(expected, result); // Implicitly calls Bar.equals, which returns true if both Bars' constructor parameters are equal. } 

Clearly, this test depends on two elements : the foo.bar() method, but also the bar.equals(otherBar) method, which is implicitly called by the assertion.

Now, I could write an other test, which asserts that this bar.equals() method works correctly. However, say it fails. Now, my first test also should fail, but for a reason beyond its scope.

My point is that, for this particular example, nothing really is problematic; I could maybe check for equality without using equals at all. However, I feel like this sort of issue will become a real problem with more complex behaviours, where avoiding existing methods because they might not work would involve rewriting large amounts of code just for tests.

How to translate these requirements into code, without making unit tests interdependent ?

Is it even possible ? If not, should I stop bothering about this issue, and assume all methods not under the current test work ?

2
  • Assuming you've got a getter to get the "42", assert that result.get() == expected.get() (since you don't need to implement the equality method for int); then assert that result.equals(expected). Commented Feb 5, 2018 at 21:59
  • I'm not sure of where you mean to put these assertions; Anyways, part of my point is that this is very easy to do, but it assumes very simple objects. However, say this Bar actually has 3 fields, with each being an instance of another class in my project. To assume that the equals function works correctly, I'd have to assume that every field's value's equals function also works correctly. This is no problem when the point is to test the equals function in itself, but my question is: what if I'm just using it to test something else ? Commented Feb 5, 2018 at 22:05

2 Answers 2

1

However, say it fails. Now, my first test also should fail, but for a reason beyond its scope.

Test isolation is a really important thing, you are right but note also that this has a limitation.
You will necessary fall into some cases where in your unit tests, you will have to rely on other general methods of other objects such as equals/hashCode or still constructors.
This is unavoidable. Sometimes, the coupling is not an issue.
For example, using a constructor to create the object passed to the method under test or a mock value is natural and should really not be avoided !

But in other cases, the coupling with the API of another class is not desirable.
It is for example the case in your sample code as your test relies on an equals() method which the fields tested may change through the time and also be functionally different from fields to watch/assert in the tested method.
In addition, this doesn't make the fields asserted explicit.

In this kind of case, to assert fields values of a returned object by the method under test, you should assert field by field via getter methods.
To avoid writing this repetitive and error prone code, I use a matcher testing library such as AssertJ or Hamcrest (included in JUnit at a time) that provides fluent and flexible way to assert the content of an instance.

For example with AssertJ :

Foo foo = new Foo(5); Bar actualBar = foo.bar(); Assertions.assertThat(actualBar) .extracting(Bar::getId) // supposing 42 is stored in id field .containsExactly(42); 

With such a simple example, matcher testing library has no real value. You could directly do :

Foo foo = new Foo(5); Bar actualBar = foo.bar(); Assert.assertEquals(42, actualBar.getId()) 

But for cases where you want to check multiple fields (very common case), it is very helpful :

Foo foo = new Foo(5); Bar actualBar = foo.bar(); Assertions.assertThat(actualBar) .extracting(Bar::getId, Bar::getName, Bar::getType) .containsExactly(42, "foo", "foo-type); 
Sign up to request clarification or add additional context in comments.

3 Comments

Interesting, since I started on such simple examples, I didn't even think that I wouldn't need a 'full equals' in most cases. Your way indeed makes more sense, as it only tests what's relevant to the test. I'm still slightly worried about code duplication across tests, but I suppose that could be solved using what the framework has to offer. I'm using JUnit and I don't plan on moving to another library just yet, but I'm guessing the functionality you're showing can be achieved with JUnit matchers ?
I find that you ask yourself very good questions. At a time, Hamcrest (the matcher library for tests) was provided with JUnit. Today, I am not sure. I updated to give references about Hamcrest and AssertJ. But adding a library in a test scope to improve the overall quality of your tests should never be an issue. It should even be the rule seeing the time that writing and maintaining units tests takes !
Thanks ! I have looked it up, and hamcrest is still a part of JUnit's standard distribution. It seems well documented, and easy to use, so it should be no issue to add it into my tests.
0

I've found it helpful to divide my classes into two main types:

  1. Types which hold data. They represent your data's structure, and contain little to no logic.
  2. Types which hold logic, and hold no state.

The reason for this is that many classes at many layers will inevitably be tied to your data model, so you want the representation of your data model to be rock solid and unlikely to yield any surprises.

If Bar is a data structure, then relying on the behavior of its .equals() method isn't much different than relying on the behavior of int or String's .equals() behavior. You should stop worrying about it.

On the other hand, if Bar is a logic class, you probably shouldn't be relying on its behavior at all when you're trying to test Foo. In fact, Foo shouldn't be creating a Bar at all (unless Foo is a BarFactory, e.g.). Instead, it should be relying on interfaces which can be mocked.

When it comes to testing your Data types (especially in the exceptional cases where the data type really needs to have some logic, like a Uri class, you will of a necessity be testing multiple methods within that class (like Bar's constructor and equals method). But the tests should all be testing that class specifically. Make sure you've got plenty of tests to keep these rock-solid.

When testing a service class (one with logic), you will effectively be assuming that any data types you're dealing with have been tested sufficiently so you're really testing the behavior of the type you're worried about (Foo) and not the other types you happen to be interacting with (Bar).

2 Comments

I have to admit - My existing code is very (very) far from the ideal division you're describing. I can see the benefit of it, though; I foresee the nightmare of unit testing monolithic structures that linger around my code. However, I intend to take unit testing as slight incremental improvements; otherwise, I doubt I'd achieve much. What you're describing is more like massive refactorings that will have to wait. When I do get to these, though, I'll remember this. :)
@Niss36: Yeah, be pragmatic when applying any programming principle. When dealing with legacy code, this is more of a direction you ought to be heading in than a destination.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.