A couple weeks ago, I had a really good chat with Reed Copsey, Jr (@ReedCopsey) regarding F#. Specifically, he recommended using F# for unit testing as a good way to start incorporating F# into your environment.
This makes sense for 2 reasons. First, unit tests are atomic, so they lend themselves to functional-style programming. Second, it isn't production code, so your team (and boss) may be less concerned about impact to the project itself.
Today, I decided to give it a try. Reed suggested FsUnit, so that's where I started. For this first experiment, I decided to TDD into FizzBuzz in C# (which I've done before). Since this is a known problem, I could concentrate on the testing bits.
Setting Up FsUnit
The first step is to get FsUnit up and running. This was pretty easy to do. FsUnit is available from NuGet, and there are some good examples for getting started as well.
I'm starting with a project that already has a shell for the FizzBuzz library:
This is a placeholder method (in C#) that takes an integer as a parameter and returns a string.
Next, I added a new F# Library project to my solution. I called it "FizzBuzz.Library.Tests":
Then I used NuGet to get FsUnit:
This package relies on NUnit under the covers, so it will also bring down the NUnit framework. As usual with NuGet packages, it adds the appropriate references to our project:
Here we can see that both "FsUnit.NUnit" and "nunit.framework" were added.
Note: if you'd rather use xUnit, there is a "FsUnit.xUnit" package.
Since I've been using NUnit quite a bit recently, I'm happy with using the default FsUnit package. This also means that we can add the NUnit Test Adapter so that our tests show up in the Visual Studio Test Explorer:
Make sure you grab the "NUnit3TestAdapter" because FsUnit uses NUnit 3.5.
Your First FsUnit Assertion
To get an idea of how to actually use FsUnit, check out the Syntax section on the "What is FsUnit?" page: http://fsprojects.github.io/FsUnit/
Here's a snippet:
The first section shows that we need to bring in the "NUnit.Framework" and "FsUnit" namespaces. The next sections show the various ways that we can do assertions.
I like the use of "should" and "should not'" using the pipe forward operator. This means that we can create some pretty readable code (similar to Fluent Assertions and contrasting with the standard "Assert" class members).
Here's a first test just to make sure that everything is set up right:
At the top, I created a module to hold the test functions. This name will show up in the test explorer, so I want it to be nice and readable.
Next, we have the two "open" statements that we saw above.
Next, we have our test function. Because we're using NUnit, we want to use the "Test" attribute for this function. (Attributes in F# use the square bracket/angle bracket syntax.)
For the function name, I'm using a sentence rather than a traditional method name. This is also what shows up in our test explorer, so we want to think about this. (Note: this varies from the 3-part naming scheme that I've use previously. I'm experimenting with something different here.)
The body of the function is pretty simple. In this case, we're piping in "1" to the "should equal 1". This is quite trivial, but it will show us that our tests are set up appropriately.
Here's the output in the Test Explorer:
Notice that our module name and the function name both show up here. Things are green which means we're running successfully at this point.
But we need a real test now.
Your First Real Test
So let's TDD into our "GetValue" method. For this we'll create a real test that calls the method:
Now we can see that the assertion syntax makes a bit more sense. Let's look at this a bit more closely.
Notice that I've included "open FizzBuzz.Library". This is the namespace where our "GetValue" method lives.
Next, the test name is "When 1 return '1'". Again, this is what will show up in our test explorer.
Finally, the body of the method shows how FsUnit makes things pretty readable. We call the "GetValue" method with our parameter, then we pipe it to the "should equal" to compare it to the string "1".
This test fails (since "GetValue" throws a NotImplementedException):
We can then update the method with the simplest code possible to get the test to pass:
And now the test passes:
This is a bit of a naive test since we know that our method needs to be a bit more complex than this, but it's a start.
Parameterizing Tests
Just like when we were writing these tests in NUnit in C#, we want to parameterize the tests so that the same function can be used for multiple test cases.
Quite honestly, I'm not sure if this is the best way to do this in F#, but this is one way that I know based on my NUnit experience. We'll use the "Values" attribute on a function parameter:
This changes our function so that it will run with the values of 1, 2, and 4. In this case, we expect that a string version of the integers comes out: "1", "2", and "4" respectively.
Notice that in the "should equal", we need to have parentheses around the "input.ToString()" call. This is because this needs to be evaluated before it is passed as a parameter to the "equal" function.
At this point, just one of the tests passes (which should not be a surprise):
The output shows us that the test that uses "1" passes, but the other two fail. So we can parameterize tests just like we do with NUnit. (Again, I'll keep an eye out to see if there's a better way to handle this in FsUnit.)
The "GetValue" method is easy enough to update to satisfy these tests:
And now the tests pass:
Yes, I realize that we put in two failing tests here, but we're looking more at how to use FsUnit as opposed to TDD methodology.
Completing the Tests
Here are the rest of the tests showing values for 1 through 20:
Because we have parameterized tests, we can cover all of these cases with only 4 test functions. The "GetValue" method has been completed at this point:
So we can see all of the passing tests in the test explorer as well:
Comparison Between FsUnit and NUnit
Here are the same tests written in NUnit (and C#):
If we look at the tests themselves, we see that we have about the same amount of code whether we use NUnit or FsUnit. But I have to say that I like the look of the FsUnit tests a bit better.
Intermediate Variable
In the NUnit tests, we have the intermediate "output" variable. We really need this for readability. Otherwise, we would end up with the "GetValue" call inside the assertion itself. And that would be a bit difficult to read.
In the FsUnit tests, we don't need the intermediate variable. Instead, we can pipe the output of the "GetValue" call straight into the assertion.
Assertion
In the NUnit tests, we have the traditional assertion where we use a function such as "AreEqual" to compare to values. This is code that I'm used to reading, but I have to say...
I like the fluent syntax of the "should equal" in FsUnit. This reads much more like a sentence, and this is the way that our brains work. So it looks a bit better to me. (This is the same reason why we want to consider something like Fluent Assertions as well).
Wrap Up
This is just a first experiment with FsUnit and using F# for unit testing (even if the production code is not F#). I like what I see so far, and I'll be looking into this further.
First, I'll take a closer look at parameterization of tests. If using the standard NUnit approach (with various types of attributes) is the way to go, then I'll stick with that. But I'm also curious if there are other ways to approach this.
In addition, I'm interested to see how things work when things get a bit more complex. FizzBuzz is an extremely simple example. The good news is that there is some guidance available on the FsUnit for NUnit page. This shows how to set up a test fixture (or multiple test fixtures) to work with objects that are in a particular state. This will be interesting to explore further.
My first impression is that I agree with Reed. This is a good way to start incorporating F# into your environment. The tests are very readable, and generally we like to keep tests simple and straightforward. In addition, since it doesn't directly impact our production code, it's an easier sell to the team. Definitely worth looking into further.
Update: I've done a bit more experimentation with mixed results. More to come!
Happy Coding!
No comments:
Post a Comment