DEV Community

Cover image for F# Unit Testing – Refining the Squirrel Simulation
Matt Eland
Matt Eland

Posted on • Originally published at killalldefects.com on

F# Unit Testing – Refining the Squirrel Simulation

This article on F# Unit Testing is part 3 in an ongoing series in which I walk you through building a genetic algorithm in F# that emulates a squirrel’s “brain” as it solves a simplistic puzzle.

Yes, really.

Previously we created an F# Console application that rendered small grid with a single squirrel in it. Next, we made that application a bit more functional and introduced other actor types and world generation.

Learning Objectives

In this article, I’m going to walk you through:

  • Unit testing in F#
  • Improving unit tests with FsUnit
  • Writing unit tests for the core game logic
  • Implementing specific features of note in the logic
  • Improving the console app by giving it color

By the end of the article we’ll have a console app where the player can control the squirrel actor, get the acorn, and return to the tree to win the game. Alternatively the player could choose to let the dog eat the squirrel or just not eat anything and starve to death – either choice ends in a loss.

The end result F# application

The final product isn’t intended to be played by humans. In future articles I’ll walk you through setting up the genetic algorithm and fitness function in order to get the squirrel to figure out how to navigate its environment and win the game on its own.

But for now, let’s get the core simulation logic sorted out so we can focus on the artificial intelligence aspects in future articles.

If you’d like to play along with the finished result, the code is available on GitHub.

F# Unit Testing

This article assumes you’re already familiar with the basic concept of a unit test, but maybe not in F#. If you’re not, take a look at the Wikipedia entry for Unit Testing to familiarize yourself.

In F#, much like in other languages, we start by creating a new unit test project to our solution, making sure to choose F# as the language.

Create a new F# Unit Test Project

We’ll select an F# xUnit project. Either XUnit, NUnit, or MSTest could work, but XUnit is most frequently used with .NET Core.

Click Next, then name the project and click create.

Next we’ll expand the project in Solution Explorer, right click on Dependencies and click Add Reference..., then select the project reference for the logic project containing most of core logic. Click OK and the reference will be added.

Adding an XUnit Test

We’re going to be using a different framework in a moment, but I want to start with testing using XUnit to show the difference later.

We’ll start with a few test cases around logic for determining that points are adjacent to one another.

In the Tests.fs file, change it to the following code:

Here we’re defining an XUnit theory, which is a parameterized test case that is invoked once per set of data. In this case we’re using InlineData to pass in positions as well as if we expect these points to be adjacent or not adjacent. These pieces of data come in via the x1, y1, x2, y2, and expectedAdjacent parameters.

We then follow an arrange / act / assert style pattern where we set up two points and check isAdjacentTo, the function under test.

Finally, we do an assert comparing the calculated value to the expected value.

Running this test can be accomplished using the Visual Studio Test Explorer view as pictured below.

F# Unit Testing in Visual Studio 2019

Improving Tests with FsUnit

Next, let’s look at integrating FsUnit into the test. This is an open source library to improve the quality of F# unit tests.

Right click on the dependencies node for your test project in Solution Explorer and select Manage NuGet Packages….

Click on Browse, then search for FsUnit

Adding a Reference

Select FsUnit and click Install. You will need to accept the external party license to install some packages.

Next, add open FsUnit to your list of imports in your test file.

You can now clean up your test to the following syntax:

Here we remove the Assert syntax which was difficult read even in C# and VB .NET. We’ll stick with this more functional assertion syntax going forward.

Testing Rabbit Movement

Now that we’ve gotten a handle on testing in F#, let’s add some tests around the core rules of simulation logic for the rabbit entity. Those rules are somewhat simple:

  • The rabbit cannot move off the edge of the map
  • The rabbit will move to a random adjacent tile that doesn’t have another actor on it

Simulating this will give us a foundation for writing tests for the more complicated actors.

Let’s take a look:

Here we define a simple function to serve as a “random number generator” stand-in function during the test run. This is faster than relying on Random and also consistent.

Next we define buildTestState to define the starting point of the simulation for most tests.

Both of these functions are defined outside of an individual test as we want to reuse them in multiple tests.

Next we set up our state and grab the rabbit’s initial position. Once simulateActors is called, we check that the rabbit’s position is not its initial position (yes, I know we’re not testing for randomness, but we are checking for movement).

The should not' (equal originalPos) syntax is a bit odd. No, the apostrophe is not a typo, that’s needed to use the not function, as are the parentheses. This appears to be a quirk around how F# unit testing works with FsUnit’s functions.

Implementing Rabbit Motion

The code to move the rabbit is fairly simple:

What we’re doing here is defining a moveRandomly function that can move any actor in any direction.

It first checks getCandidates to get a sequence of adjacent cells that the actor is allowed to move into. It then sorts each possibility by a random number (which in our tests will always be 42), and finally it grabs the first (or head) element of that sequence.

Once the destination is picked, moveActor is invoked to carry out the move and build out and return the new state:

MoveActor defined
Nested functions collapsed for now for readability

Let’s ignore the 3 collapsed nested functions for now and focus on the tail end of this function. We match on the target to see if something is present. If not, go ahead and perform the move, which will change the actor’s position:

Note: I’m not wild about this code in general. Actor management is likely the area I’d like to improve the most of the code thus far.

If another actor was detected in the position the actor was about to move into, the simulation checks to make sure that the executing actor is allowed to enter the other actor’s tile. For example, a dog is not allowed to enter a tree due to its limited climbing ability.

In cases where there is an actor and the movement is allowed, we match on which actor is actually moving and we invoke one of two special commands, which we’ll explore later on as we discuss the dog and the squirrel.

Testing Dog Movement

Okay, so now that we looked at how the rabbit works and how movement works in general, let’s look a bit more closely at the dog:

The dog’s logic is to remain stationary and eat the rabbit or the squirrel if they come near it.

For this test we’re going to manipulate the test’s starting state to have the squirrel already adjacent to the dog. We’re then going to call simulateActors and ensure that the resulting state has the following traits:

  • The dog is in the squirrel’s former tile
  • The squirrel is no more
  • The game is now lost

We accomplish these things in the handleDogMove function which was mentioned earlier but not expanded. Let’s look at this function now:

Again we see a bit of special-casing based on which actor is being eaten. This repetition or near repetition is a definite code smell and one I’ll look to correct as the series goes on.

Essentially what we’re doing here is modifying the game state to have a new snapshot of the world in which the dog has changed positions and the squirrel or rabbit is dead (depending on which it ate). If the squirrel died, the game should also now be over.

That simple logic is all we need to have a dog that sits and waits for something to come near it to be eaten.

Implementing a Timer

While we’re on the subject of things that can kill the squirrel, let’s talk about hunger.

Hunger is a concept we’re implementing to enforce a time limit on the simulation to either get the acorn and win or meet the dog and lose. This is important since a large number of our randomized squirrel genomes won’t be effective and we don’t want those trying too long before giving up.

The unit tests around hunger are as follows:

Both of these are similar, in nature, and both rely on the turn processing logic baked into the simulator:

Testing Squirrel Movement

The squirrel is where the remaining bits of core logic are going to exist.

Recall from last article that the simulation ends in a win when the squirrel enters the acorn tile to get the acorn, then goes to the tree tile. Let’s write some tests around this behavior.

F# Unit Testing and Squirrel Brains

Here we test that a squirrel without an acorn (indicated by ActorKind = Squirrel false), if positioned to the right of an acorn in a test map, should get the acorn if it moves left.

We evaluate success by the boolean hasAcorn field on the Squirrel ActorKind and expect it to be true, indicating the squirrel has the acorn.

This is accomplished via the handleSquirrelMove function we discussed earlier. I’ll show that more in a moment, but first lets look at the return to tree with the acorn test:

This is a very similar test to the last one, only we define the Squirrel as a Squirrel true to indicate that it has the acorn. We position it next to the tree and we verify after the move that the squirrel is now in the tree and the game is set to won.

Squirrel Internals

So how does the squirrel logic work?

It turns out the squirrel works in a fairly simple way. When a squirrel is bumping into another actor, we need to handle one of two special cases differently:

  1. The Squirrel doesn’t have the acorn and is entering the acorn tile. In this case the simulation replaces the squirrel with one that has the acorn and sets the acorn to IsActive = false so it doesn’t render / trigger events.
  2. The squirrel has the acorn and is entering the tree. In this case, the simulation ends with the squirrel in the tree.

That’s it! It’s effectively a reducer, albeit one that’s a bit messy to read. I’d like to find some easier syntax for this sort of operation as I’m not wild about the overall look of this function, but it does the job.

Bringing Color to the World

Now that we have tests around all of the game rules, lets polish up the console app by adding colors and the timer display.

Let’s start by updating the displayWorld function:

Here starting at line 14, we manipulate Console.ForegroundColor based on the type of actor we’re rendering. This is somewhat brittle in cases where we change what character an actor generates, but I didn’t want to put this logic in the shared class library.

Note that all I have to do here is set the ForegroundColor and .NET takes care of everything else for me. If I wanted, I could also set BackgroundColor here. Both run off of a restricted 16 color palette that you cannot customize, so your rendering options are limited.

Finally, at the end, I set the ConsoleColor back to white so we don’t accidentally render UI prompts in an unexpected color.

End Result and Next Steps

As of the end of this article, we’ve used F# Unit Testing to build a fully-functional squirrel game. It’s incredibly easy and boring, but it’s not really intended for a human to play it.

The F# console app in action
Illustrating the various outcomes of the game. Note the dog eating the rabbit early on.

Next time we’ll move from the console world to the desktop. From there, we’ll be able to get into the depths and details on genetic algorithms as we explore how to roll our own artificial intelligence to evolve the perfect squirrel.

Stay tuned.

The post F# Unit Testing – Refining the Squirrel Simulation appeared first on Kill All Defects.

Top comments (0)