How I Create Test Objects in Java

Date:

The Problem

When I write tests in Java, I find it tedious to manually create objects and setting attributes. Code easily turns out like this:


  var foo = new Foo("Satellite", 1000, true);
  var bar = new Bar("Edward", "Scissorhands", List.of(foo));
  // do stuff
  assertEquals(foo, bar.findByName("Satellite"));

Here we're first creating a Foo, which is then added to a Bar. We then test that we can find the Foo when we ask the Bar to search for it.

It is clear that even though both Foo and Bar have other attributes, we're only interested in setting the name, and making sure that we can find it later. All the other ones are just in the way in this test.

Also, having to do this for every test is cumbersome and boring, and will probably at some point be the reason why I choose to not write a test. Boooh.

The Solution

What I normally do is to set up test object builders that makes it really easy to create new objects on the fly. For the above classes I would create two builder like this:


  public class FooTestDataCreator {

      private Faker faker = new Faker();
      private String name = faker.name().lastName();
      private int flurbs = faker.number().numberBetween(1,2000);
      private boolean blarg = faker.bool();

      public static FooTestDataCreator aFoo() {
              return new FooTestDataCreator();
      }

      public FooTestDataCreator withName(String name) {
          this.name = name;
          return this;
      }

      public FooTestDataCreator withFlurbs(int flurbs) {
          this.flurbs = flurbs;
          return this;
      }

      public FooTestDataCreator withBlarg(boolean blarg) {
          this.blarg = blarg;
          return this;
      }

      public Foo build() {
          return new Foo(name, flurbs, blarg);
      }
  }
  
  public class BarTestDataCreator {

      private Faker faker = new Faker();
      private String firstName = faker.name().firstName();
      private int lastName = faker.name().lastName();
      private List<Foo> foos = List.of(aFoo().build());

      public static BarTestDataCreator aBar() {
              return new BarTestDataCreator();
      }

      public BarTestDataCreator withFirstName(String firstName) {
          this.firstName = firstName;
          return this;
      }

      public BarTestDataCreator withLastName(String lastName) {
          this.lastName = lastName;
          return this;
      }

      public BarTestDataCreator withFoos(List<Foo> foos) {
          this.foos = foos;
          return this;
      }

      public Bar build() {
          return new Bar(firstName, lastName, foos);
      }
  }

With these two builders, the test code would look like this:


  var foo = aFoo().withName("Satellite").build();
  var bar = aBar().withFoos(List.of(foo)).build();
  // do stuff
  assertEquals(foo, bar.findByName("Satellite"));

It is not much less code if you're counting characters, but it states intent more clearly by only setting the value we actually care about.

The values that are not explicitly set gets a random value with the help of Faker. This means that we easily can create valid objects, and the random values assert that we're not accidentally writing code that just works for the hard coded test value.

Conclusion

By writing builders like above for my test objects, I find my tests easier to both read and write, and they're more reliable. It does take some time to write them, but I absolutely find it worth my time.