11.2.3 Test Combination functions
In section 11th. 1.2, when we discuss the dependencies in the tracking code, the C # method used, similar to the F # functions in the last two examples, demonstrates functional programming to make it easier to identify what the function does and what data to access. This is not only useful when writing code, but also extremely useful when testing.
In section 11.1, we have written an imperative method of printing a multi-word name, but it has side effects and removes the element from the Mutable list passed in as a parameter. As long as we no longer use this list, it will not cause any problems. Any unit tests on this method to check the printout will succeed.
The tricky thing about this approach is that if we use it in conjunction with other equally correct methods, we may get unexpected results, so it's hard to test the command code thoroughly. In principle, what we should test is that each method only does what it should do, and only this. Unfortunately, the "and this only" section is really hard to test because any piece of code can access and modify any part of the shared mutable state.
In functional programming, we don't modify any shared state, so we just need to verify that the function returns the correct result for all given inputs. This also shows that when we use two tested functions together, as long as the test combination has a corresponding result: there is no need to verify that the function does not break the data in a subtle way. The test shown in listing 11.11 does not seem to make sense at all, but imagine what it would look like if we were using LIST<T>, rather than the immutable F # list.
Listing 11.11 testing two functions with side effects (F #)
[<fact>]
Let partitionthenlongest () =
Lettest = ["Seattle"; "New York"; "Grantchester"]
letexpected = ["New York"], ["Seattle"; "Grantchester"]
Letactualpartition = Partitionmultiword (Test) | [1]
Letactuallongest = getlongest (Test) |
Assert.equal (expected,actualpartition) | [2]
Assert.equal ("Grantchester", actuallongest) |
It can be found that unit tests run two functions [1] sequentially, but only partial results are used, and the results are the same as our expectations [2]. In this way, the function calls are independent, and if they do not contain any side effects, we are free to change the order of the calls. In the world of functions, this unit test is not required at all: we have written unit tests for each individual function, and this test does not validate any additional behavior.
However, if we want to write a similar code using a variable list<t> type, this test may catch the error we found in section 11.1. If the Partitionmultiword function modifies the list referenced by the value test, such as deleting all the names of the words, then the result of the second call cannot be the "Grantchester" of the test expected. This is a general idea of the importance of functional code: If we test all the basic parts well and then test the combined code, then we don't need to test in the new configuration whether the basic parts still have the correct behavior.
So far, we have discussed the refactoring and testing of functional programs. We found that the one-time function reduces code duplication, that immutable data structures help us understand what the code is doing, and that it reduces the need to test two pieces of code that might interfere with each other.
The remainder of this chapter will discuss how you can take advantage of this when code is executed to make your code more efficient. First, we need to understand when there is some flexibility, and how F # and C # decide when to execute code.
11.2.3 Test Combination functions