Tangles in Test Code: Magic Values

Michael Kutz
5 min readMar 13, 2024
Some test code with lots of magic values and some symbols in the foreground.

There is a certain sloppiness in the industry when writing test code. This causes what I call tangles. A very common one is the usage of magic values in test code.

Why is this a Tangle?

Magic values (most well known as Magic numbers) are a commonly known anti-pattern in programming. It generally is

A unique value with unexplained meaning or multiple occurrences (…)

This is bad in production code as we have no clue why the particular value was chosen and hence cannot decide if we can (ever) change it to something else.

While this is considered really bad in production code, it seems to be perfectly fine for test code. At least according to my experience with actual test suites.

I guess we (yes I did it myself several times) consider test code more like a workbench where we want to change things around. Or like a list of random examples that should or should not do something. Or like place to experiment — more like a worksheet and less like something that we will need to maintain. The problem with these notions, though: it is wrong!

Test code needs to be maintained. It is essential when we refactor the production code. It is useful as a living documentation. It helps us to guide our changes in small steps.

Magic values make test maintenance unnecessarily hard. At best they leave us guessing if a certain value was chosen randomly or intentionally and the intentionally chosen values are not obvious (they may seem now, but the might not be to another developer or even to yourself next month).

Even worse: the value was consciously chosen. For example it needs to be valid in some way. In that case, when the valid range changes, we will need to crawl through the test code and update these values where ever we used them. The classic duplicate code problem.

Consider this example:

@Test
void age_almost_birthday() {
var gilly =
new Unicorn(
"c048f205-937b-421f-8b93-ae251e1ca7df",
"Gilly",
ManeColor.RED,
111,
11,
LocalDate.now().minusYears(62).minusMonths(1).minusDays(2));

assertThat(gilly.age()).isEqualTo(62);
}

The only consciously chosen value in this test is 62. And it occurs twice for a reason, but that reason is not made obvious. All the other values are just noise. You might have the habit to always choose numbers with only 1 digits and hence clearly see that 111 and 11 are not important. You might always use the name “Gilly” if that’s not important. And RED might be the first item in the MadeColor enumeration. This all might be obvious to you while you write this test, but it will make you or somebody else do some unnecessary guesses when reading this test, like

  • Do we need to put a UUID string as ID?
  • Are there some constraints on the name?
  • Is the ManeColor of any importance to this test?
  • Are 111 and 11 representatives of some value range?

How to Untangle?

Explicit Constants

One quite obvious solution to avoid magic values is the usage of constants. These could be defined locally in the test case, in the test class or even in a globally accessible collection class. So in case the values need to be changed, it can be done in one place.

Another advantage is the fact that constants have names, which may express why a certain value was chosen. E.g. a constant SOME_MANE_COLOR expresses that the actual value is not considered important, while VALID_HORN_LENGTH suggests that the value was chose form a certain valid value range.

The above example with explicit constants might look like this:

class TestConstants {
public final String SOME_UUID = "c048f205-937b-421f-8b93-ae251e1ca7df";
}

class UnicornTest {
private final String SOME_NAME = "Gilly";
private final ManeColor SOME_MANE_COLOR = ManeColor.RED;
private final int VALID_HORN_LEGTH = 111;
private final int VALID_HORN_DIAMETER = 11;

@Test
void age_almost_birthday() {
var gilly =
new Unicorn(
TestConstants.SOME_UUID,
SOME_NAME,
SOME_MANE_COLOR,
VALID_HORN_LEGTH,
VALID_HORN_DIAMETER,
LocalDate.now().minusYears(62).minusMonths(1).minusDays(2));

assertThat(gilly.age()).isEqualTo(62);
}
}

Random Data Generators

While explicit constants may express the actual value should not be important, the usage of randomly generated data even verifies that it actually isn’t.

The above example might look like this using random data generators and explicit constants:

class UnicornTestDataGenerators {
public String randomUnicornId() {
return UUID.randomUUID();
}
public String randomUnicornName() {
return RandomStringUtils.randomAlphabetic(8, 16)
}
public int randomHornLength() {
return random.nextInt();
}
public int randomHornDiameter() {
return random.nextInt();
}
}

@Test
void age_almost_birthday() {
var gilly =
new Unicorn(
randomUnicornId(),
randomUnicornName(),
SOME_MANE_COLOR,
randomHornLength(),
randomHornDiameter(),
LocalDate.now().minusYears(62).minusMonths(1).minusDays(2));

assertThat(gilly.age()).isEqualTo(62);
}

This usage of randomness is considered bad by a number of people for a very good reason: All these values might be different in each run of the test. So if the test fails with one value in you continuous integration, it might succeed when you run it locally or the other way around.

I personally am OK with that, as long as the random values are really not important to the test at hand. In the above example they really aren’t. They simply need to be present to create an instance and will not be read by the method under test.

If the random values are actually used in the test, I’d at least need them to appear in the test report somehow, so I can replace the random data generator with the failing value to reproduce the test. I’d likely achieve that with parametrized tests.

If you like randomness in testing, please consider property-based testing, which combines randomness with built-in repeatability (e.g. jqwik for Java).

Test Data Builder or Arrange Helper Method

I already wrote about Test Data Builders and Arrange Helper Methods in Tangles in Test Code: Long Arrange/Given/Setup. I just want to point out that while the improved tests above clearly steer my attention to the one interesting value in the arrange step, it still contains a lot of uninteresting lines of code. Using a specialized Arrange Helper Method to createUnicornBornAt(…) or a UnicornTestDataBuilder to create aUnicorn.birthday(…) effectively makes the values invisible and also makes the test independent of the constructor.

Conclusion

Magic values are bad in production code and are just as bad in test code. Don’t tolerate them. Remove them with the above recipes to keep your test code more readable and maintainable.

The code blocks in this article are partly simplified versions of example in the Untangle Your Spaghetti Test Code workshop, I created with Christian Baumann. You can find the full code and a lot more tangles on GitHub.

--

--

Michael Kutz

I've been a software engineer since 2009, worked in various agile projects & got a taste for quality assurance. Today I'm a quality engineer at REWE digital.