LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 56: Expectation at eighty percent

The original article was published on January 30, 2025.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Expectation at eighty percent

The following JsonStringWriter is an interface for saving JSON strings to local storage. Let's assume that the argument of the write function must be a String for a certain reason.

interface JsonStringWriter {
    fun write(jsonString: String)
}

The following FooClass is an example of using JsonStringWriter.

class FooClass(private val jsonStringWriter: JsonStringWriter) {
    fun fooFunction() {
        ...
        val jsonString = ...
        jsonStringWriter.write(jsonString)
    }
}

Let's write a unit test for this FooClass. The following test function writeJsonString_withImportantValue verifies the behavior of FooClass.fooFunction. In this unit test, to avoid actual writing to local storage, a Mock of JsonStringWriter is created using Mockito and passed to the constructor of FooClass.

    @Test
    fun writeJsonString_withImportantValue() {
        val jsonStringWriter: JsonStringWriter = mock()
        val subject = FooClass(jsonStringWriter)

        subject.fooFunction()
        verify(jsonStringWriter).write(
            """
            {
                "important_value": "cat",
                "unrelated_object": {
                    "many": "other",
                    "unrelated": "values"
                }
            }
            """.trimIndent()
        )
    }

Is there any problem with this test?

False expectations

In this test, the expected value used in verify is directly specified as a JSON string, which causes the following issues:

  1. Lack of validation for expected value: It cannot be guaranteed that the expected value string is valid as JSON. If both the production code and the test have the same mistake (e.g., missing double quotes for "cat"), the test will pass.
  2. Unnecessary strictness: If the given value is valid as JSON, changes in formatting such as indentation or line breaks should be allowed as part of the specification. However, the current test strictly checks even the number of spaces, resulting in a false-positive test. If the JSON string conversion logic is updated, all calling tests also need to be updated.
  3. Ambiguity of test purpose: From the test function name, it can be inferred that the purpose of this test is to verify that "important_value" is "cat". "unrelated_object" seems to be outside the scope of the test, but if it doesn't match, the test will not pass. Therefore, it is unclear whether the verification of "unrelated_object" is included in the test's purpose.

To solve these issues, it is better to abstract the expected value. In the improved test below, argThat is used to clearly verify the argument from two perspectives.

  1. The given string is valid as JSON.
  2. The JSON string has a key "important_value" with the value "cat".
    @Test
    fun writeJsonString_withImportantValue() {
        val jsonStringWriter: JsonStringWriter = mock()
        val subject = FooClass(jsonStringWriter)

        subject.fooFunction()
        verify(jsonStringWriter).write(
            argThat { jsonString ->
                val json = JsonParser.parseString(jsonString).getAsJsonObject()
                json.get("important_value").asString == "cat"
            }
        )
    }

However, there are still some issues with this test.

  1. Unnecessary knowledge of implementation details: The detailed behavior of the JSON parser may vary depending on the library. However, this test assumes that JsonStringWriter internally uses JsonParser (or behaves equivalently).
  2. Code duplication: If JsonStringWriter is widely used, similar code will be written in many tests. Every time the specification of JsonStringWriter changes, the tests also need to be updated, potentially causing bugs.

To avoid these issues, if the dependent code is complex, it is better to create a mock of the test double. A "mock" in a test double is a test object that can verify function calls and arguments (note that some mock libraries like Mockito use the term "Mock" differently). The following MockJsonStringWriter is an example of a mock in a test double.

class MockJsonStringWriter : JsonStringWriter {
    private var latestJson: JsonElement? = null

    override fun write(jsonString: String) {
        latestJson = try {
            JsonParser.parseString(jsonString).getAsJsonObject()
        } catch (e: ...) {
            throw AssertionError("...", e)
        }
    }

    fun verifyPropertyAsString(name: String, value: String) {
        assertNotNull(latestJson, "...")
        assertEquals(value, latestJson.get(name).asString, "...")
    }
}

This mock has two verification functions.

  1. The given string is valid as a JSON string.
  2. The given JSON string has the specified string property.

By using this mock, the calling test can be simplified as follows. The implementation of JsonStringWriter and the behavior of MockJsonStringWriter need to be consistent, but individual test code does not need to bear that responsibility. Even if the implementation changes, the impact on the tests can be confined within MockJsonStringWriter.

    @Test
    fun writeJsonString_withImportantValue() {
        val mockJsonStringWriter = MockJsonStringWriter()
        val subject = FooClass(mockJsonStringWriter)

        subject.fooFunction()
        mockJsonStringWriter.verifyPropertyAsString("important_value", "cat")
    }

In this way, by abstracting the expected value in the test code, the purpose of the test can be clarified, and the maintainability of the test itself can be improved.

In a nutshell

Abstract the expected value. Consider creating a test double mock if the dependent code is complex.

Keywords: test double, mock, expected value

List of articles on techniques for improving code quality