#blog #Fsharp #BDD #TDD The exciting adventures of me as I stumble my way through F#, Gherkin, and assorted useful libraries...
After merging back to master and creating a new feature branch (feature-loading) it's time to make that loading screen do something. Currently it's a dead end, as it has no allowed actions.
Actually, the loading screen shouldn't have any allowed actions. It needs to progress to the arena screen on its own. After it's finished loading. But there is nothing to load yet, so it would go directly to the arena, which would make testing whether actions are unavailable impossible.
But that's fine, because I'm interested in testing the loading screen, not the loading. I can provide some mock loading to happen, and can configure it to take a specific amount of time to load, as necessary for testing.
What is the purpose of a loading screen? It is to provide users with something to look at while they are unable to interact with the system. And to potentially provide information about the progress of the loading process, such as how much loading remains (and maybe even how long that might take).
Some kind of Loader object will do the actual loading, and the loading screen will report on its progress, irrespective of whether the Loader is a 'real' loader or a testing loader.
In case it isn't obvious, for the tests (and users) to test/see the progress of the loading while the loading is happening, the loading needs to be asynchronous.
Actually, now that I think about it, it seems polite to wait for the user to be ready before continuing on the the Arena, after loading. Therefore, the Continue action will be available when loading is complete, but not before.
The status of loading progress should update regularly so the user does not have to wonder whether the loading has stalled (unless it has).
Feature: Loading Screen
In order to comfortably wait for the Arena to load
Users of Rolbots Arena should be able to
See the progress of the loading process, updated at least every second, and will be informed that they can proceed to the Arena, when the loading is finished.
Scenario: Cannot continue when loading is in progress
Given a user who is at the arena loading screen
And loading is not finished
When the user attempts to continue
Then they remain at the loading screen
Scenario: Loading progress updates
Given a user who is at the arena loading screen
And loading is not finished
And they took note of the progress status
When the user waits for a second
Then the progress status has changed
Scenario: User is informed of completed loading
Given a user who is at the arena loading screen
And loading is not finished
When loading finishes
Then they are notified of the option to continue, within one second
Scenario: User can continue on to the Arena
Given a user who is at the arena loading screen
And loading is finished
When the user attempts to continue
Then the arena screen is shown next
Now this is some more interesting behavour to test!
I'm not going to bother making cases for both new and returning users for all of these. I will include that in the unit tests though. But for here I'll just go with new users because that's simpler.
Much of the step implementation is straightforward. I had to determine how to wait a second, but that wasn't difficult. I'm yet to implement telling the controller than loading is complete though. Largely because I don't yet know what is informing the controller.
let wait seconds =
async{
do! Async.Sleep (1000*seconds)
} |> Async.RunSynchronously |> ignore
let [<Given>]``a user who is at the arena loading screen`` () =
let controller = Controller UserFamiliarity.New
controller.DoAction UserAction.Continue
controller
let [<Given>]``loading is not finished``(controller:Controller) =
controller
let mutable status = ""
let [<Given>]``they took note of the progress status``(controller:Controller) =
status<-controller.Status
controller
let [<When>]``the user attempts to continue``(controller:Controller) =
controller.DoAction UserAction.Continue
controller
let [<Then>]``they remain at the loading screen``(controller:Controller) =
Assert.Equal(Screen.ArenaLoading, controller.CurrentScreen)
let [<When>]``the user waits for a second``(controller:Controller) =
wait 1
controller
let [<Then>]``the progress status has changed``(controller:Controller) =
Assert.NotEqual<string>(status, controller.Status)
let [<When>]``loading finishes``(controller:Controller) =
controller
//todo: work out how to do this
let [<Then>]``they are notified of the option to continue, within one second``(controller:Controller) =
wait 1
Assert.Equal<IEnumerable<UserAction>>([UserAction.Continue], controller.AvailableActions)
let [<Given>]``loading is finished``(controller:Controller) =
controller
//todo: work out how to do this
The test explorer shows how long each test takes:
1) Rolbot.XunitFeature.Loading(scenario: Scenario: Cannot continue when loading is in progress)
Duration: 36 ms
2) Rolbot.XunitFeature.Loading(scenario: Scenario: Loading progress updates)
Duration: 1 sec
Message:
Assert.NotEqual() Failure
Expected: Not ""
Actual: ""
Stack Trace:
at LoadingSteps.the progress status has changed(Controller controller) in LoadingSteps.fs line: 38
3) Rolbot.XunitFeature.Loading(scenario: Scenario: User is informed of completed loading)
Duration: 1 sec
Message:
Assert.Equal() Failure
Expected: FSharpList<UserAction> [Continue]
Actual: FSharpList<UserAction> []
Stack Trace:
at LoadingSteps.they are notified of the option to continue, within one second(Controller controller) in LoadingSteps.fs line: 46
4) Rolbot.XunitFeature.Loading(scenario: Scenario: User can continue on to the Arena)
Duration: 11 ms
Message:
System.InvalidOperationException : Unknown scene: arena
Stack Trace:
at CommonSteps.the (arena loading|main menu|settings|arena) screen is shown next(String nextscreen, Controller controller) in Common.fs line: 13
As intended, the tests which wait for a second take a second.
I might as well just make that a simple function, and I can complicate matters later when necessary. That's the TDD way!
let [<When>]``loading finishes``(controller:Controller) =
controller.FinishedLoading()
controller
Now the scenario steps are filled out, the project compiles, and the behaviour tests run and fail (well the "cannot continue from loading screen" scenario passes because that's defualt behaviour), so it's time to write a unit test.
module Rolbot.Tests.Loading
open Xunit
open Rolbot
open System.Collections.Generic
let controllerLN () =
let controller = Controller UserFamiliarity.New
controller.DoAction UserAction.Continue
controller
let controllerLR () =
let controller = Controller UserFamiliarity.Returning
controller.DoAction UserAction.Continue
controller.DoAction UserAction.Arena
controller
let controllerFW c () =
let controller:Controller = c()
controller.FinishedLoading()
wait 1
controller
let loadings =
seq{
yield testSolo controllerLN
yield testSolo controllerLR
}
let [<Theory; MemberData("loadings")>]``no options when load not finished`` c =
let controller:Controller = c()
let options = []
Assert.Equal<IEnumerable<UserAction>>(options, controller.AvailableActions)
let [<Theory; MemberData("loadings")>]``no screen change when load not finished`` c =
checkUnavailableActions c
let [<Theory; MemberData("loadings")>]``continue option within a second of finished`` c =
let controller:Controller = controllerFW c ()
let options = [UserAction.Continue]
Assert.Equal<IEnumerable<UserAction>>(options, controller.AvailableActions)
let [<Theory; MemberData("loadings")>]
``continue to arena works when load finished`` c =
let controller:Controller = controllerFW c ()
Assert.Equal(Screen.Arena, controller.CurrentScreen)
let [<Theory; MemberData("loadings")>]``other actions should do nothing after load finished`` c =
checkUnavailableActions (controllerFW c)
Okay I wrote a few (and changed the root namespace to Rolbot because it was inconsistent). The basic ones pass automatically.
I also made an important performance related change:
let checkUnavailableActions controllerSource =
let controller:Controller = controllerSource()
let options = controller.AvailableActions
let screen = controller.CurrentScreen
property{
let! action = GenX.auto<UserAction> |> GenX.notIn options
controller.DoAction action
return controller.CurrentScreen = screen
} |> Property.check
Because otherwise it waits a second multiplied by 60, and that's terrible.
Controller.FinishedLoading() needs to alter the value of AvailableActions for the ArenaLoading screen. I can add a new mutable field (initialised to false) and change it in FinishedLoading()
member __.FinishedLoading() =
loadFinished <- true
And then I add a case for it in AvailableActions (which had been dropping to the default empty case for ArenaLoading).
member __.AvailableActions =
match currentScreen with
| Screen.Title -> [Continue]
| Screen.MainMenu -> [Arena;Settings]
| Screen.Settings -> [MainMenu]
| Screen.ArenaLoading ->
match loadFinished with
| false -> []
| true -> [Continue]
| _ -> []
Next, I need the Continue action to go to the Arena screen when appropriate.
member __.DoAction action =
currentScreen <-
match action with
| _ when not (List.contains action __.AvailableActions) -> currentScreen
| UserAction.Settings -> Screen.Settings
| UserAction.Arena -> Screen.ArenaLoading
| UserAction.MainMenu -> Screen.MainMenu
| UserAction.Continue ->
match currentScreen with
| Screen.Title ->
match userFamiliarity with
| New -> Screen.ArenaLoading
| Returning -> Screen.MainMenu
| Screen.ArenaLoading ->
match loadFinished with
| false -> currentScreen
| true -> Screen.Arena
| _ -> currentScreen
That looks... atrocious actually. Also the test doesn't pass: it's still on ArenaLoading when it should be on Arena. But the Continue action is definitely available so...
Oh, I'm not actually doing the action. Duh.
``continue to arena works when load finished`` c =
let controller:Controller = controllerFW c ()
controller.DoAction UserAction.Continue
Assert.Equal(Screen.Arena, controller.CurrentScreen)
That works, so now I can refactor the awful mess of nested matches. Oh and while I'm at it, the common step for checking the Screen needs to have Arena added to it:
let [<Then>]``the (arena loading|main menu|settings|arena) screen is shown next`` nextscreen (controller:Controller) =
Assert.Equal(
match nextscreen with
| "arena loading" -> Screen.ArenaLoading
| "main menu" -> Screen.MainMenu
| "settings" -> Screen.Settings
| "arena" -> Screen.Arena
| s -> invalidOp ("Unknown scene: " + s)
, controller.CurrentScreen)
Now only the 'progress updates before a second passes' scenario is failing. That doesn't have a unit test yet. But I'll still refactor first.
member __.DoAction action =
currentScreen <-
match action with
| _ when not (List.contains action __.AvailableActions) -> currentScreen
| UserAction.Settings -> Screen.Settings
| UserAction.Arena -> Screen.ArenaLoading
| UserAction.MainMenu -> Screen.MainMenu
| UserAction.Continue ->
match currentScreen, userFamiliarity, loadFinished with
| Screen.Title, New, _ -> Screen.ArenaLoading
| Screen.Title, Returning, _ -> Screen.MainMenu
| Screen.ArenaLoading, _, true -> Screen.Arena
| _ -> currentScreen
Much neater and still passes the same tests.
Now to make a unit test for the progress updates. I'll have it check over more than one second. 6 should do. But doing it twice is overkill.
let [<Fact>]``progress status updates every second for 6 seconds`` () =
let controller = controllerLR()
let rec checkStatusChanged times previous =
match times with
| 0 -> ()
| _ ->
wait 1
Assert.NotEqual<string>(previous, controller.Status)
checkStatusChanged (times-1) controller.Status
checkStatusChanged 6 controller.Status
The status just has to be different every time it retrieved.
member __.Status =
status <- status + "."
status
That's a quick and dirty implementation (ugh, mutables!) but there's no reason to do more since nothing more is specified yet. This will get revisited when there's soemthing to load, and the test is in place and is passing in order to keep the future implementation in spec. Yay!
Now it's Arena time!