This is Jawher Moussa's blog

in which he writes about technical stuff

Java vs Go: Impressive solutions for invented problems

Let’s start with the Shadoks

Les Shadoks is a cartoon created by Jacques Rouxel which was first broadcast in France between 1968 and 1974.

Les Shadoks series had a lasting cultural impact, and introduced some lovely phrases/expressions that, to this day, are used in the day to day discussions:

When one tries continuously, one ends up succeeding. Thus, the more one fails, the greater the chance that it will work

or:

Every advantage has its disadvantages and vice versa

or even:

If there is no solution, it is because there is no problem.

But most importantly:

Why do it the easy way when you can do it the hard way?

Why the Shadoks reference ?

I’ve been writing Java professionally for the last 13 years. Luckily, I also work with other languages, mostly Go and Javascript.

Working with Go made me notice a pattern in the Java ecosystem: it is a wide and rich one, with very solid & technically impressive libraries & frameworks.

But the thing is, many of those libraries & frameworks are impressive solutions for a problem which didn’t need exist in the first place: trying to use a declarative approach everywhere.

Case in hand: JUnit tests

In the following, I’ll be using a (unit) test written in Java and showcasing JUnit, which is arguably the most popular and used testing framework in the Java land, in its latest version at the time this post was written, version 5 codenamed Jupiter.

There will be a couple of code snippets, so please bear with me till I make my point at the end.

In java, everything is a class

The same applies to tests:

import org.junit.jupiter.api.Test;

public class ExampleTest {

    @Test
    void test_login() {
        assertThat(login("username", "p@$$w0rd")).isTrue();
    }
}

As can be seen in the code snippet above, a test is:

Test cases

What if we’d like to test our login logic against multiple username and password combinations ?

One way to do it would be to create a new test method for every combination:

import org.junit.jupiter.api.Test;

public class ExampleTest {

    @Test
    void accepts_correct_login1() {
        assertThat(
                login("username", "p@$$w0rd")
        ).isTrue();
    }

    @Test
    void rejects_incorrect_username() {
        assertThat(
                login("incorrect-username", "p@$$w0rd")
        ).isFalse();
    }

    @Test
    void rejects_incorrect_password() {
        assertThat(
                login("username", "incorrect-password")
        ).isFalse();
    }
}

This quickly gets unwieldy when:

Parameterized tests

That’s why JUnit offers a way to write the test method only once, and invoke it as many times as we provide test cases:

import org.junit.jupiter.api.Test;

public class ExampleTest {

    @ParameterizedTest
    @MethodSource("loginTestCases")
    void test_login(String username, String password, boolean expected) {
        assertThat(
                login(username, password)
        ).isEqualTo(expected);
    }

    private static Stream<Arguments> loginTestCases() {
        return Stream.of(
                Arguments.of("username", "p@$$w0rd", true),
                Arguments.of("incorrect-username", "p@$$w0rd", false),
                Arguments.of("username", "incorrect-p@$$w0rd", false)
        );
    }
}

Lots of noise, but basically:

Test case naming

In the testing report, and by default, parameterized test methods will get a dynamically generated name which is the combination of all the parameters it receives.

This can be tweaked using a templated string:

@ParameterizedTest(name = "login({0}, {1}) => {2}")

Where {0}, {1}, … get replaced by the method argument in the corresponding position.

Execution order

Say we have 2 cases that must run in a specific order, e.g.:

The code would look like this:

public class ExampleTest {
    private String token;

    @Test
    void testLogin() {
        String token = login("username", "p@$$w0rd")
        assertThat(token).isNotNull();

        this.token = token;
    }

    @Test
    void testApiCall() {
        int res = apiCall(this.token);

        assertThat(res).isEqualTo(42);
    }
}

Except this won’t work as expected:

But not to worry: more annotations to the rescue:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ExampleTest {
    private String token;

    @Test
    @Order(1)
    void testLogin() {
        String token = login("username", "p@$$w0rd")
        assertThat(token).isNotNull();

        this.token = token;
    }

    @Test
    @Order(2)
    void testApiCall() {
        int res = apiCall(this.token);

        assertThat(res).isEqualTo(42);
    }
}

Conditional tests

What if we want to only run a specific test method when a certain condition is set ? Say for example we have a slow test we would like to only run on a beefy CI agent, based on E2E env var for example.

    @Test
    @EnabledIfEnvironmentVariable(named = "E2E", matches = "true")
    void testSlowApiCall() {
        int res = apiCall(this.token);

        assertThat(res).isEqualTo(42);
    }

What’s the Go way of doing the same ?

Test cases

Go has:

func TestLogin(t *testing.T) {
	type Case struct {
		Username string
		Password string
		Expected bool
	}

	for _, tt := range []Case{
		{"username", "p@$$w0rd", true},
		{"incorrect-username", "p@$$w0rd", false},
		{"username", "incorrect-p@$$w0rd", false},
	} {
		t.Run(fmt.Sprintf("login(%s, %s) => %v", tt.Username, tt.Password, tt.Expected), func(t *testing.T) {

			require.Equal(t, tt.Expected, login(tt.Username, tt.Password))
		})
	}
}

I make use of testify toolkit to simplify assertions.

Order, keeping state

Go executes instructions from top to bottom. Unless you exit the current scope, the variables keep their values.

func TestLoginAPICall(t *testing.T) {
	token := ""
	t.Run("login", func(t *testing.T) {
		token = login("username", "p@$$w0rd")
		require.NotEmpty(t, token)
	})

	t.Run("api call", func(t *testing.T) {
		res := apiCall(token)
		require.Equal(t, 42, res)
    })
}

Conditional execution

Go has if switch:

func TestLogin(t *testing.T) {
	token := ""
	t.Run("login", func(t *testing.T) {
		token = login("username", "p@$$w0rd")
		require.NotEmpty(t, token)
	})

	if os.Getenv("E2E")!="" {
		t.Run("slow api call", func(t *testing.T) {
			res := slowApiCall(token)
			require.Equal(t, 42, res)
		})
    }
}

Closing words

Something went very wrong in the Java ecosystem:

for some reason, we as a community, collectively decided that:

It is most impressive that we got so far with these self-imposed limitations.

I am not picking on JUnit. On the opposite: what they are able to achieve is simply impressive. Everything is very extensible and configurable, and it must have taken lots of time & effort to reach this point.

Yet, the Go testing library achieves the same level of power/flexibility while being much simpler and with a tinier surface area simply by choosing an imperative model (t.Run() vs @Test), making it possible to use the full power of the host language.

Thoughts ?

Disqus went to shit. So I got rid of it in this reincarnation.

I’m looking into other options (utteranc.es, commeto.io, remark42, …)

In the meantime, please feel free to yell at me in twitter for example: