F#nctional Gravitation with Expecto

Let’s develop a minuscule F# library, AstroSharp.Core”, to compute the magnitude of the Gravitational force between two bodies. Subsequently, let’s create a unit testing library, “AstroSharp.Tests”, to validate our module function.

We’ll be using Visual Studio Code on a Mac with Ionide, Mono, Paket and FAKE. Expecto, a super awesome F# testing library will be used for our Unit Tests.

Creating the Project Directory Structure

If you are a newcomer with none of the aforementioned libraries / applications installed, I would suggest following option #2, here.

I am currently using the following versions:

Mono - 5.0.0.1 | Ionide - 2.26.1 | Paket - 1.6.3 | FAKE - 1.2.3

Let’s navigate to an appropriate folder, and create a directory “AstroSharp”

Next, time to blast open our Visual Studio Code with the empty contents of our AstroSharp folder and Create 2 projects named: AstroSharp.Core and AstroSharp.Tests.

Assuming all the required supporting libraries are installed, we create the two folders by opening the Code Command Pallet using Cmd + Shift + P, selecting the F#: New Project option.

Next, we select the Class Library option for AstroSharp.Core and follow the setup accordingly:

Once completed, our directory looks like:

We repeat the process for AstroSharp.Tests except, this will be Console Application. Our directory, after this step, will look like:

Alright, we have all our projects in place, let’s add the Expecto library to AstroSharp.Tests using Paket. This is done by:

  1. Opening up the fsproj file for AstroSharp.Tests [ AstroSharp.Tests.fsproj ]
  2. Opening the Command Pallet, like before by pressing Cmd + Shift + P and then typing PAKET and choosing the “Add Nuget Package (to current project)” option.

3. Enter in “Expecto” and hitting enter.

4. We confirm the successful addition of Expecto to our library by making sure our output tab doesn’t show any errors:

And ensuring the reference to Expecto.dll was added to fsproj.

One last step before we dive into the code would be to build the two projects to make sure everything is building.

Before, let’s insure AstroSharp.Core compiles by removing the automatically generated AstroSharp.Core type from AstroSharp.Core.fs. Our AstroSharp.Core.fs should look like:

We invoke FAKE’s Build Default option and hope and pray.

If the stars have properly lined up, our build output should look like:

The Gravitation Function

Now that the boring stuff is out of the way, let’s concentrate on the code.

Let’s start by including a function in the Gravitation module in AstroSharp.Core that computes the magnitude of the Gravitation Force specified by Newton’s Law of Gravity:

Where:

m1 and m2 are the masses of the two bodies, G is the gravitational constant and r is the distance between the two bodies. F, of course, is the force vector.

Without too much ceremony, we jot down the function in the following manner in AstroSharp.Core.fs

namespace AstroSharp.Core
module Gravitation =
    let G = 6.67408 * ( 10.0 ** -11.0 )
    let getGravitationForce m1 m2 r = 
if r = 0.0 || m1 < 0.0 || m2 < 0.0 then None
else
Some ( G * m1 * m2 / ( r * r ))

Our preconditions are that:

  1. None of the masses can be negative. [ Although, scientists have recently produced negative mass ]
  2. Distance between two bodies cannot be 0 as this would imply, the gravitation force is infinite which is an impossibility because of electromagnetic repulsion that keeps the atomic particles separated.

Unit Tests

Once our Core function is written, we want to add a reference of AstroSharp.Core in AstroSharp.Tests. This can be done in the following by going to AstroSharp.Tests.fsproj and opening up our trustworthy command pallet with “F#: Add Project Reference” selected.

The project to edit is AstroSharp.Tests.fsproj.

The project reference is the AstroSharp.Core.fsproj one.

We confirm the successful reference addition by locating the AstroSharp.Core.fsproj reference in AstroSharp.Tests.fsproj.

Next, we want to write our unit tests. For the sake of simplicity, we are including 4 tests:

  1. Test #1: “0 Distance Between Bodies” expected to return None
  2. Test #2: “Mass of Body 1 is Negative” expected to return None
  3. Test #3: “Mass of Body 2 is Negative” expected to return None
  4. Test #4: “Gravitation Smoke Test” expected to return Some expected value.

We bunch up these tests using a testList named “Gravitation Tests”.

module AstroSharp.Tests
open Expecto
open AstroSharp.Core.Gravitation
let gravitationTests = 
testList "Gravitation Tests" [
...
]

Our first test expecting a None result looks like:

     test "0 Distance Between Bodies" {
let noneResult = getGravitationForce 10.0 10.0 0.0
Expect.equal noneResult None "Result is None since the distance is 0"
}

Our next 2 tests look like:

test "Mass of Body 1 is Negative" {
let noneResult = getGravitationForce -1.0 10.0 0.0
Expect.equal noneResult None "Result is None since the mass of the 1st body is negative"
}
test "Mass of Body 2 is Negative" {
let noneResult = getGravitationForce 10.0 -1.0 0.0
Expect.equal noneResult None "Result is None since the mass of the 2nd body is negative"
}

Our smoke test looks like:

test "Gravitation Smoke Test" {
let someResult = getGravitationForce 10.0 10.0 1.0
Expect.isSome someResult "Result has a Value i.e. not None"
Expect.floatClose Accuracy.medium someResult.Value ( G * 100.0 ) "Result is 100 times G"
}

Our main entry point is:

[<EntryPoint>]
let main argv =
runTestsWithArgs defaultConfig argv gravitationTests

Altogether, our test code looks like:

module AstroSharp.Tests
open Expecto
open AstroSharp.Core.Gravitation
let gravitationTests = 
testList "Gravitation Tests" [
test "0 Distance Between Bodies" {
let noneResult = getGravitationForce 10.0 10.0 0.0
Expect.equal noneResult None "Result is None since the distance is 0"
}
        test "Mass of Body 1 is Negative" {
let noneResult = getGravitationForce -1.0 10.0 0.0
Expect.equal noneResult None "Result is None since the mass of the 1st body is negative"
}
        test "Mass of Body 2 is Negative" {
let noneResult = getGravitationForce 10.0 -1.0 0.0
Expect.equal noneResult None "Result is None since the mass of the 2nd body is negative"
}
        test "Gravitation Smoke Test" {
let someResult = getGravitationForce 10.0 10.0 1.0
Expect.isSome someResult "Result has a Value i.e. not None"
Expect.floatClose Accuracy.medium someResult.Value ( G * 100.0 ) "Result is 100 times G"
}
]
[<EntryPoint>]
let main argv =
runTestsWithArgs defaultConfig argv gravitationTests

To run our tests, we use Expecto’s Run:

We should see the 4 tests pass with the following output:

Conclusion

In this blogpost, we used Visual Studio Code, Ionide, Paket and FAKE to create two projects: our core library and a console application with our unit tests; the tests were written with the help of Expecto.

We barely scratched the surface of the extent of Expecto that is literally an arsenal of tools to improve testing our libraries in a functional way.

The code for this project can be found: https://github.com/MokoSan/AstroSharp/

Show your support

Clapping shows how much you appreciated Moko Sharma’s story.