Writing Empirical Tests

Empirical tests are common in unit testing—you call a method and verify that it did what you expected. Such tests help us to verify that, as code evolves, the expectations are still met and the code continues to work as intended.

Empirical tests are useful for functions and methods that are deterministic and don’t have any dependencies that hold state. We’ll create a few empirical tests for the Airport class now.

First, let’s create some properties in the test suite using the yet-to-be-written Airport class. For this, within the AirportTest class, let’s define a few sample properties before the init() function:

 val​ iah = Airport(​"IAH"​, ​"Houston"​, ​true​)
 val​ iad = Airport(​"IAD"​, ​"Dulles"​, ​false​)
 val​ ord = Airport(​"ORD"​, ​"Chicago O'Hare"​, ​true​)

We can write a new test, right after the canary test, to exercise the properties of Airport, like so:

 "create Airport"​ {
  iah.code shouldBe ​"IAH"
  iad.name shouldBe ​"Dulles"
  ord.delay shouldBe ​true
 }

Try running the build, and you’ll notice it fails because the Airport class doesn’t exist yet. Let’s define the Airport class, in the file Airport.kt, under the directory src/main/kotlin/com/agiledeveloper/airportstatus:

 package​ ​com.agiledeveloper.airportstatus
 
 data class​ Airport(​val​ code: String, ​val​ name: String, ​val​ delay: Boolean)

We defined the Airport class as a data class with three properties. No further code is needed for the class at this time. Run the build and verify that it passes.

The previous test was trivial, but it helped us to get the properties in place. Let’s write a test for a sort() method that will take a list of airports and return them sorted by their names.

When driving the design of code using tests, we use the first few tests to help define the interface of methods and then a few more tests to bring in the necessary implementation. In that spirit, let’s start with a small test that passes an empty list to the sort() method and expects an empty list as the result:

 "sort empty list should return an empty list"​ {
  Airport.sort(listOf<Airport>()) shouldBe listOf<Airport>()
 }

This test will fail since the sort() method doesn’t exist in the Airport class. Let’s create that method now. In the test, we invoked the sort() method directly on Airport, instead of on an instance of Airport. Thus, sort() has to be a method in a companion object of Airport, like so:

 data class​ Airport(​val​ code: String, ​val​ name: String, ​val​ delay: Boolean) {
 companion​ ​object​ {
 fun​ ​sort​(airports: List<Airport>) : List<Airport> {
 return​ airports
  }
  }
 }

To satisfy the new test, which passes in an empty list to sort(), we merely have to return the given list—that’s exactly what we did in sort(), in the spirit of writing the minimum code to make the tests pass. Run the build, and verify that all three tests we have so far pass.

This test helped us to focus on the method name, the parameter type, and the return type; that is, it drove the design of the method signature. Let’s write another test for the sort() method, this time passing in a list with one element.

 "sort list with one Airport should return the given Airport"​ {
  Airport.sort(listOf(iad)) shouldBe listOf(iad)
 }

After writing this test, run the build. All tests, including the new one, should pass. That tells us that the current implementation suffices for the tests we have in place so far.

Next let’s write a test that takes two airports, but already in sorted order:

 "sort pre-sorted list should return the given list"​ {
  Airport.sort(listOf(iad, iah)) shouldBe listOf(iad, iah)
 }

There should be no issues with the build passing to run this and all the other tests we wrote previously. It’s time to take the leap for the sort to actually have the necessary implementation.

 "sort airports should return airports in sorted order of name"​ {
  Airport.sort(listOf(iah, iad, ord)) shouldBe listOf(ord, iad, iah)
 }

The output of the sort() method should be in sorted order of the airports, based on the name of the airports. If we run the build now the new test will fail. Let’s modify the sort() function to make all the tests we have so far pass.

 fun​ ​sort​(airports: List<Airport>) : List<Airport> {
 return​ airports.sortedBy { airport -> airport.name }
 }

Run the build and verify all tests pass. When writing automated tests, we should explore different reasonable edge cases and verify that the code behaves appropriately. As an exercise, write a few more tests for the sort() method. For example, what if two airport names start with the same substring? After all, some cities have multiple airports and we want to make sure the sorting works according to the specifications. Tests represent specifications—if our code has to handle edge cases, it’s a good practice to express the behavior for such situations in tests first, and then implement the necessary minimum code.

When developing code using tests, we’ll want to verify multiple scenarios or combinations of input to functions. However, if we write one test for each combination, the test file can get long and verbose. This is where data-driven tests come in, to reduce the noise and make tests concise, as we’ll see next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset