Say I have various input strings that are passed into the same function and returns output strings that are compared against their expected results. For example, periods should be transformed to dashes and vice versa.
class SomeClassUnitTest {
private val someClass = SomeClass()
@Test
fun exampleTest_SomeInput_ReturnsCorrectOuput() {
val input = "..."
val output = someClass.someFunction(input)
val expected = "---"
assertEquals(expected, output)
}
@Test
fun exampleTest_SomeInput_ReturnsCorrectOuput() {
val input = "---"
val output = someClass.someFunction(input)
val expected = "..."
assertEquals(expected, output)
}
}
I have a couple questions about the best practice and practicality of another way to structure this unit test. First being, if I'm testing the same function with similar inputs and outputs, I can structure the unit test like the above code or like so:
@Test
fun exampleTest_SomeInputs_ReturnsCorrectOuputs() {
val input = listOf{ "...", "---" }
val output = listOf<String>()
input.forEach { output += someClass.someFunction(input) }
val expected = listOf{ "---", "..." }
assertEquals(expected, output)
}
But SHOULD I structure the test like this? I figure this would make the unit test class more concise and readable so in case there's other functions in SomeClass to test then the test can be added into this file with another list of values to test against.
I can accept if it's wrong to develop a unit test like this. We do lose out on the convenience of the test function name describing how the function's unit test is failing. And I suppose the unit test can be over-engineered to tell the developer what specific value is failing the test. What are your thoughts?
Another aspect I want to ask is about unit test efficiency? Could the more concise unit test be more performant than individual tests for each input?
I know it wouldn't be apparent from the examples I've provided, so what if the function took a bit of time to complete? I'm thinking the list of values could all be initiated asynchronously and once all responses are complete then the comparison against the expected result is performed. Thus the method of a unit test for individual inputs as O(k*n)
(where k
is the amount of tests and n
is the processing time) is reduced to O(n)
which is determined now by the longest processing time of the list of values.
I'm mostly curious about what the community has found in their own experience. I could test these myself, but I'm concerned with the first question, "should I structure the test like this?" and the second is more of a bonus granted it's possible to perform asynchronous operations in unit tests.
I've been through a similar line of thought. In many cases the solution you propose makes it more difficult to identify the cases that fail. It will also prevent your test from aborting after encountering a failure. Additionally, it makes it harder to customize test cases.
For your specific case I would probably write something like this:
fun exampleTest_SomeInput_ReturnsCorrectOuput() {
assertEquals(someClass.someFunction("..."), "---");
assertEquals(someClass.someFunction("---"), "...");
}
I have written tests like this with 10 or 20 asserts. You can see they are not all the same.
@Test
fun unitsFromString() {
Assert.assertEquals(null, Units.fromString(""))
Assert.assertEquals(Mile, Units.fromString("miles"))
Assert.assertEquals(Mile, Units.fromString("mile"))
Assert.assertEquals(Mile, Units.fromString("mi"))
Assert.assertNotEquals(Mile, Units.fromString("m"))
Assert.assertEquals(Meter, Units.fromString("m"))
Assert.assertEquals(Kilometer, Units.fromString("km"))
Assert.assertEquals(Kilometer, Units.fromString("kilometer"))
Assert.assertEquals(Kilometer, Units.fromString("kilometers"))
Assert.assertEquals(Inch, Units.fromString("in"))
Assert.assertEquals(Foot, Units.fromString("ft"))
Assert.assertNotEquals(Foot, Units.fromString("f"))
Assert.assertEquals(MetersPerSecond, Units.fromString("m/s"))
Assert.assertEquals(Kilogram, Units.fromString("kg"))
Assert.assertEquals(Pound, Units.fromString("pound"))
Assert.assertEquals(Pound, Units.fromString("pounds"))
Assert.assertEquals(Pound, Units.fromString("lb"))
}
Test performance is a consideration but I value readable results over speed. Especially when you are talking about tests that run in the 0-10 ms range. I introduced a failure in the test above. Android Studio makes it easy to identify the failure and jump to the exact line of code. If you were to store all the results in an array prior to asserting them, it would be more difficult to determine which input value or values caused the failure.