Unit testing F# libraries in .NET Core using dotnet test and NUnit
This tutorial takes you through an interactive experience building a sample solution step-by-step to learn unit testing concepts. If you prefer to follow the tutorial using a pre-built solution, view or download the sample code before you begin. For download instructions, see Samples and Tutorials.
This article is about testing a .NET Core project. If you're testing an ASP.NET Core project, see Integration tests in ASP.NET Core.
- .NET Core 2.1 SDK or later versions.
- A text editor or code editor of your choice.
Creating the source project
Open a shell window. Create a directory called unit-testing-with-fsharp to hold the solution. Inside this new directory, run the following command to create a new solution file for the class library and the test project:
dotnet new sln
Next, create a MathService directory. The following outline shows the directory and file structure so far:
/unit-testing-with-fsharp unit-testing-with-fsharp.sln /MathService
Make MathService the current directory and run the following command to create the source project:
dotnet new classlib -lang "F#"
You create a failing implementation of the math service:
module MyMath = let squaresOfOdds xs = raise (System.NotImplementedException("You haven't written a test yet!"))
Change the directory back to the unit-testing-with-fsharp directory. Run the following command to add the class library project to the solution:
dotnet sln add .\MathService\MathService.fsproj
Creating the test project
Next, create the MathService.Tests directory. The following outline shows the directory structure:
/unit-testing-with-fsharp unit-testing-with-fsharp.sln /MathService Source Files MathService.fsproj /MathService.Tests
Make the MathService.Tests directory the current directory and create a new project using the following command:
dotnet new nunit -lang "F#"
This creates a test project that uses NUnit as the test framework. The generated template configures the test runner in the MathServiceTests.fsproj:
<ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" /> <PackageReference Include="NUnit" Version="3.9.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.9.0" /> </ItemGroup>
The test project requires other packages to create and run unit tests.
dotnet new in the previous step added NUnit and the NUnit test adapter. Now, add the
MathService class library as another dependency to the project. Use the
dotnet add reference command:
dotnet add reference ../MathService/MathService.fsproj
You can see the entire file in the samples repository on GitHub.
You have the following final solution layout:
/unit-testing-with-fsharp unit-testing-with-fsharp.sln /MathService Source Files MathService.fsproj /MathService.Tests Test Source Files MathService.Tests.fsproj
Execute the following command in the unit-testing-with-fsharp directory:
dotnet sln add .\MathService.Tests\MathService.Tests.fsproj
Creating the first test
You write one failing test, make it pass, then repeat the process. Open UnitTest1.fs and add the following code:
namespace MathService.Tests open System open NUnit.Framework open MathService [<TestFixture>] type TestClass () = [<Test>] member this.TestMethodPassing() = Assert.True(true) [<Test>] member this.FailEveryTime() = Assert.True(false)
[<TestFixture>] attribute denotes a class that contains tests. The
[<Test>] attribute denotes a test method that is run by the test runner. From the unit-testing-with-fsharp directory, execute
dotnet test to build the tests and the class library and then run the tests. The NUnit test runner contains the program entry point to run your tests.
dotnet test starts the test runner using the unit test project you've created.
These two tests show the most basic passing and failing tests.
My test passes, and
Fail every time fails. Now, create a test for the
squaresOfOdds method. The
squaresOfOdds method returns a sequence of the squares of all odd integer values that are part of the input sequence. Rather than trying to write all of those functions at once, you can iteratively create tests that validate the functionality. Making each test pass means creating the necessary functionality for the method.
The simplest test we can write is to call
squaresOfOdds with all even numbers, where the result should be an empty sequence of integers. Here's that test:
[<Test>] member this.TestEvenSequence() = let expected = Seq.empty<int> let actual = MyMath.squaresOfOdds [2; 4; 6; 8; 10] Assert.That(actual, Is.EqualTo(expected))
Notice that the
expected sequence has been converted to a list. The NUnit framework relies on many standard .NET types. That dependency means that your public interface and expected results support ICollection rather than IEnumerable.
When you run the test, you see that your test fails. You haven't created the implementation yet. Make this test pass by writing the simplest code in the Library.fs class in your MathService project that works:
let squaresOfOdds xs = Seq.empty<int>
In the unit-testing-with-fsharp directory, run
dotnet test again. The
dotnet test command runs a build for the
MathService project and then for the
MathService.Tests project. After building both projects, it runs your tests. Two tests pass now.
Completing the requirements
Now that you've made one test pass, it's time to write more. The next simple case works with a sequence whose only odd number is
1. The number 1 is easier because the square of 1 is 1. Here's that next test:
[<Test>] member public this.TestOnesAndEvens() = let expected = [1; 1; 1; 1] let actual = MyMath.squaresOfOdds [2; 1; 4; 1; 6; 1; 8; 1; 10] Assert.That(actual, Is.EqualTo(expected))
dotnet test fails the new test. You must update the
squaresOfOdds method to handle this new test. You must filter all the even numbers out of the sequence to make this test pass. You can do that by writing a small filter function and using
let private isOdd x = x % 2 <> 0 let squaresOfOdds xs = xs |> Seq.filter isOdd
Notice the call to
Seq.toList. That creates a list, which implements the ICollection interface.
There's one more step to go: square each of the odd numbers. Start by writing a new test:
[<Test>] member public this.TestSquaresOfOdds() = let expected = [1; 9; 25; 49; 81] let actual = MyMath.squaresOfOdds [1; 2; 3; 4; 5; 6; 7; 8; 9; 10] Assert.That(actual, Is.EqualTo(expected))
You can fix the test by piping the filtered sequence through a map operation to compute the square of each odd number:
let private square x = x * x let private isOdd x = x % 2 <> 0 let squaresOfOdds xs = xs |> Seq.filter isOdd |> Seq.map square
You've built a small library and a set of unit tests for that library. You've structured the solution so that adding new packages and tests is part of the normal workflow. You've concentrated most of your time and effort on solving the goals of the application.