11.2.1.2 Writing unit tests in F #
If we write the code for direct testing in this way, it is easy to change it into a unit test and become part of a large project. Soon, we'll talk about how to do this with xunit.net, but for now, we're going to write another call that should be explicitly overwritten by the unit test: Using a null value as the parameter value, call the Getlongest function:
> getlongest (null);;
Program.fs (24,12): Error fs0043:the type ' stringlist '
does not has ' null ' as a proper value
This, as we have not tried before, can see that the F # Interactive console reports a compile-time error, not an exception, so we can't even write this code, which means that if we only use this function in F #, we don't need to test at all. Type values declared in F # (including differential unions, records, and class declarations for F #) do not allow null values at all, and they must be initialized to valid values. In the fifth chapter we already know that the correct way to represent missing values in F # is to use the option type, but this rule is used only in F # and is used to declare types in F #. When calling the usual. Net method, with an existing. NET type as a parameter, you can specify NULL as a valid parameter value.
Attention
Other languages, such as C #, do not understand the limitation that F # types do not allow null values. Therefore, F # functions, such as getlongest, can still receive NULL as a parameter value if called from C #. We can check this in the function by using the generic value unchecked.defaultof< ' T>, which is an unsafe way to create a null value for any reference type in F # or to get the default value of a value type; in other words, it is equivalent to C # The default (T) in the. In addition, this technique can be used to write unit tests to verify the behavior of the function. This is not often required because the public API of the F # Library tends to use standard. NET types such as seq < ' t>, which uses NULL as a valid value, so we can write unit tests in the usual way with this API.
We're only going to use this simple function in F #, so you don't have to consider the case where a C # user is called with NULL as a parameter value. Listing 11.8 shows several additional tests that we have added. Note that a large part of the code for the manifest is the version of listing 11.7, which interactively tests the slightly modified function. The most obvious difference is that we have packaged the test code inside the function and then added a feature to mark the Xunit.net test.
Listing 11.8 Verifying the behavior of functions with unit tests (F #)
#if INTERACTIVE
#r @ "C:\Programs\Development\xUnit\xunit.dll"
#endif
Open Xunit
Let getlongest (names:list<string>) =
Names|> List.maxby (fun name, name. Length)
C Requires First of the
Longest elements
Module longesttests =
[<fact>] <--marking test with attributes
Letlongestofnonempty () =
Lettest = ["Aaa"; "BBBBB"; "CCCC"] | Interactive Test after adjustment
Assert.equal ("bbbbb", Getlongest (test)) |
[<fact>]
Letlongestfirstlongest () =
Lettest = ["Aaa"; "BBB"] | [2]
Assert.equal ("Aaa", Getlongest (test)) | Expect the first longest element
[<fact>]
Letlongestofempty () =
Lettest = [] | [3]
Assert.equal ("", Getlongest (test)) | For empty lists, expect empty strings
In addition to packing each test into a function, we also create a module to keep all the cells tested in a class, technically, it's not necessary, but it's a good idea to separate the test from the subject of the program. Depending on your preference, you can move the test to the end of the file, or to a separate file in the project, or even to a separate project.
The Xunit.net framework uses the Fact attribute value to mark a method, which represents a unit test [1]. We can apply this to an F # function declaration that has a let binding because it compiles into a method. The first Test in the module was the one we wrote to interactively test the code-adjusted version, and we added two new tests.
The second Test [2] verifies that the Getlongest function, when there are several elements, returns the first one with the maximum length element. The Maxby function in the F # Library conforms to this rule, but it is not archived and may depend on the specific implementation, so it is a good idea to test it explicitly. The final Test [3] examines whether an empty string is returned when we pass an empty list to the function, which is a boundary condition that is worth considering. For example, when a result is displayed in the user interface, returning an empty string may be the desired behavior. As you may have guessed, our original execution did not conform to this rule. If you run the Xunit.net graphical user interface in a compiled assembly, you will get a result similar to Figure 11.1.
Figure 11.1 When the parameter value of a test function is an empty list, an exception is thrown instead of an empty string.
Now that we have clarified the expected behavior of the Getlongest function, we can easily correct it by adding a pattern that matches the empty list:
Let getlongest (names:list<string>) =
Matchnames with
| []–> ""
| _-> names |> List.maxby (fun name, name. Length)
After this modification, all three unit tests were passed. So far, all the tests are fairly simple, and we just need to check if the returned string matches the expected one. Typically, unit testing is more tricky than this. We will now look at the more complex functions of testing, especially when the function returns the list, comparing the actual values with the expectations.
11.2.1.2 Writing unit tests in F #