There will be no more updates to Elmer. It was fun while it lasted, but I'm pretty sure Elmer won't work with Elm 0.19.1 or later versions, and I'm not planning to make any more updates.
Instead, you should use elm-spec.
Elm-spec is a test framework for Elm that lets you describe the behavior of Elm programs, much like Elmer did. Unlike Elmer, elm-spec doesn't use kernel/native code so it has been published to the Elm package repository and should be more resilient to future changes in Elm.
Elmer makes it easy to describe the behavior of Elm HTML applications. If you love TDD and you love Elm, then you'll probably appreciate Elmer.
Behavior-driven development is a great practice to follow when writing applications. If you describe the behavior of your app in code, it's easy to add new features or refactor the code with confidence: just run the tests to see if your app still has all the behavior you've described in those tests.
Elm is a really great language for writing web applications. However, practicing BDD
in Elm can be difficult. The two main functions in the Elm architecture -- view
and
update
-- return opaque types, which cannot be inspected for testing purposes.
Even so, calling view
or update
directly requires knowledge of an application's
implementation: the shape of its model, its messages, and so on. If writing
tests requires knowledge of implementation details, you lose the biggest
benefit of writing tests in the first place: the ability to change your code
with confidence.
Elmer allows you to describe the behavior of your app without knowledge of
implementation details. It simulates the Elm architecture, calling view
and update
as necessary throughout the course of your test. It lets
you manage how commands and subscriptions are processed so you can
describe the behavior of your app under whatever conditions you need. Elmer
allows you to write tests first, which gives you the freedom and confidence
to change your code later on.
Because Elmer uses some native Javascript code to accomplish its magic, you cannot install Elmer through the elm package repository. Instead, you can install Elmer with the elmer-test
package on NPM. Follow these steps to TDD bliss ...
First, you'll need to install
- Elm (0.19)
- The latest version of the node test runner
for the elm test package that works with Elm 0.19 (
elm-test@elm0.19.0
) - This package, which you'll install via npm
I recommend installing these dependencies locally in your project directory so you can track versions carefully. Here's the command to install all these at once:
$ npm install --save-dev elm elm-test@elm0.19.0 elmer-test
Now install the elm test library:
$ npx elm install elm-explorations/test
In your elm.json
file, you'll need to manually add elmer to the test-dependencies
section like so:
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.1.0",
"elm-explorations/elmer": "6.0.0"
},
"indirect": {}
}
The latest version of Elmer is 6.0.0. Make sure the version number of elmer
matches the version number of the elmer-test
NPM package.
Notice the indirect
section under test-dependencies
. Elmer itself has the following dependencies:
"dependencies": {
"elm/browser": "1.0.0 <= v < 2.0.0",
"elm/core": "1.0.0 <= v < 2.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",
"elm/json": "1.0.0 <= v < 2.0.0",
"elm/random": "1.0.0 <= v < 2.0.0",
"elm/url": "1.0.0 <= v < 2.0.0",
"elm-explorations/test": "1.0.0 <= v < 2.0.0"
},
If any of these dependencies are not already listed as direct or indirect dependencies of your app, you'll need to list these in the indirect
section of your test-dependencies
.
If you just try to run elm-test (see below) and you're missing any dependencies, the compiler will give you an error message. Take the missing dependencies it mentions and list them as indirect test dependencies.
Now that everything's in place, you're ready to write tests with Elmer. In order to run those tests, you'll need to set the ELM_HOME
environment variable to the home
directory under the elmer-test
install. If you've installed elmer-test
locally, the directory should look like this:
<Project Home>/node_modules/elmer-test/home
I recommend adding a test script to your package.json
that sets the environment variable for you. The following will work on a Mac running bash:
"scripts": {
"test": "ELM_HOME=$(pwd)/node_modules/elmer-test/home elm-test"
}
Note that ELM_HOME
must be an absolute path (thus the $(pwd)
in the test command).
The elm
command searches for test dependencies any time you invoke it (so, even if you aren't running tests). This means that you will need to set the ELM_HOME
environment variable as described above, any time you invoke the elm
command. For example, to build your app, you'll need to do something like:
$ ELM_HOME=$(pwd)/node_modules/elmer-test/home elm make src/Main.elm
- Removed
Elmer.Http
and the dependency on elm/http.Elmer.Http
now lives in its own package so it can be updated independently. - Provided new APIs useful for creating extensions and custom matchers. See
Elmer.Value
,Elmer.Message
,Elmer.Message.Failure
,Elmer.Effects
, andElmer.Task
- Support for calling a spy across multiple test states
- Revised
Elmer.Spy
api to make it simpler and to allow the compiler to do type checking when injecting a spy or providing a fake implementation. This should provide better feedback when working with spies - See the section at the end of this document on migrating tests from 4.0.0 to 5.0.x
- Updated Elmer to work with Elm 0.19
- Revised api for targeting Html elements to allow the compiler to provide better feedback
- See the section at the end of this document on migrating tests from 3.3.1 to 4.0.0
- Last version for Elm 0.18
Read the latest documentation.
If you're interested in Elmer for Elm 0.18, you should read the documentation for Elmer 3.3.1, which you can find here.
While tests can be written with Elmer in a variety of ways, the goal is use Elmer to describe behavior rather than implementation details. What do I mean? Let's say that an application is a collection of behaviors. Each behavior has some pre-conditions -- these are characteristics of the world outside the application that must be true for the behavior to occur -- and some set of resulting post-conditions -- characteristics of the world outside the application that are a consequence of the behavior. To describe the behavior of an application, then, is to describe all the relationships that hold between relevant states of the world outside the application due to the use of that application.
Here's an example of a behavior from some game application that displays high scores:
- Given that there is an HTTP web service that responds with a 200 status and a JSON document that lists the high scores in some format.
- When the user starts the game application, then the high scores are displayed as list items in HTML.
This behavior links one state of the world -- where there is an HTTP web service that successfully
returns a JSON document in some known format -- and another -- where some HTML document contains
several <li>
elements whose text shows the high scores from the web service.
To describe this behavior, we should not care how the application accomplishes the mapping between these two states. We only care about describing the two states. To make the test pass, we will need to provide some implementation, but the test gives us freedom to choose whatever implementation makes sense for us now. Most importantly, however, as we add new behaviors to our application, we will be able to refactor our code with confidence. No matter what implementation we end up with, we should still be able to run this test and ensure that the same mapping between pre-conditions and post-conditions still holds.
Like I've said, you can use Elmer to write tests in a variety of ways, but I encourage you to
write tests that describe behavior so that you can refactor your code with confidence later on. This means
your tests should know as little as they can about the implementation of your Elm application. Strive to write
tests that do not know the shape of your model or the particular messages that flow through the update function.
Don't unit test functions. Begin each test only with references to the functions that must exist -- view
, update
,
init
-- and use Elmer to describe the pre- and post-conditions associated with some behavior.
To begin a test with Elmer, you need to generate a TestState
value. There are a variety of ways to do this:
- Use
givenElement
,givenApplication
,givenDocument
, orgivenWorker
from theElmer.Program
module to test particular kinds of programs. In these cases, you'll useElmer.Program.init
to provide an initial model and command. - Use
Elmer.given
to test an arbitrary model, view method, and update method. - Use
Elmer.Command.given
to test a command-generating function in isolation.
Since Elm is primarily designed for writing HTML applications, much of the work that goes into describing the pre- and post-conditions that characterize some behavior will involve working with HTML elements.
Elmer allows you to simulate events on elements and examine the state of elements. In order to do either, you'll need to first target an element.
Use Elmer.Html.target
along with the functions from Elmer.Html.Selector
to target an element. Here's
a partial test that targets all the <li>
elements that are children of an <ol>
with a class scores
in the current view:
allTests : Test
allTests =
describe "My Fun Game"
[ describe "High Score Screen"
[ test "it shows the high scores" <|
\() ->
Elmer.Program.givenElement App.view App.update
|> Elmer.Program.init (\_ -> App.init testFlags)
|> Elmer.Html.target
<< Elmer.Html.Selector.childrenOf
[ Elmer.Html.Selector.tag "ol"
, Elmer.Html.Selector.class "score-list"
]
<< Elmer.Html.Selector.by
[ Elmer.Html.Selector.tag "li" ]
...
See Elmer.Html.Selector
for more examples of selectors. It's also possible to write your own.
Once you target an element, that element is the subject of subsequent actions, until you target another element. The following functions define actions on elements:
- Click events:
Elmer.Html.Event.click <testState>
- Input events:
Elmer.Html.Event.input <text> <testState>
- Custom events:
Elmer.Html.Event.trigger <eventName> <eventJson> <testState>
- There are also events for mouse movements, and checking and selecting input elements. See the docs for more information.
You can make expectations about targeted elements with the Elmer.Html.expect
function.
First, specify whether you want to match against a single element (with element
)
or a list of elements (with elements
).
Then you provide the appropriate matchers for the element or the list. You can also
expect that an element exists with the elementExists
matcher.
See Elmer.Html.Matchers
for a full list of matchers. Let's add to the example above to make
an expectation about the elements we targeted. We'll use Elmer.expectAll
to chain together several
assertions. We'll expect that the list of <li>
we've targeted has 2 elements, with the first containing
text of "700 Points" and the second "900 Points".
allTests : Test
allTests =
describe "My Fun Game"
[ describe "High Score Screen"
[ test "it shows the high scores" <|
\() ->
Elmer.Program.givenElement App.view App.update
|> Elmer.Program.init (\_ -> App.init testFlags)
|> Elmer.Html.target
<< Elmer.Html.Selector.childrenOf
[ Elmer.Html.Selector.tag "ol"
, Elmer.Html.Selector.class "score-list"
]
<< Elmer.Html.Selector.by
[ Elmer.Html.Selector.tag "li" ]
|> Elmer.Html.expect (Elmer.Html.Matchers.elements <|
Elmer.expectAll
[ Elmer.hasLength 2
, Elmer.atIndex 0 <| Elmer.Html.Matchers.hasText "700 Points"
, Elmer.atIndex 1 <| Elmer.Html.Matchers.hasText "900 Points"
]
)
]
]
Commands describe actions to be performed by the Elm runtime; the result of a command depends on the state of the world outside the Elm application. Elmer simulates the Elm runtime in order to facilitate testing, but it is not intended to replicate the Elm runtime's ability to carry out commands. Instead, Elmer allows you to specify what effect should result from running a command. This is one important way that Elmer allows you to describe the conditions that characterize an application behavior.
Suppose there is a function f : a -> b -> Cmd msg
that takes two arguments and produces a command. In order
to specify the effect of this command in our tests, we will override occurrences of f
with a function we create in our tests. This function will generate a special command
that specifies the intended effect, and Elmer will process the result as if the original command were actually performed.
For a more concrete example, check out this article, which discusses how to fake commands and subscriptions during a test.
Note that while Elmer is not capable of processing any commands, it does support
the general operations on commands in the core Platform.Cmd
module, namely, batch
and map
. So, you
can use these functions as expected in your application and Elmer should do the right thing.
Elmer provides built-in support for navigation commands. If you want to work with Http during your tests, check out the Elmer.Http extension.
Elmer provides support for functions in the Browser.Navigation module that allow you to handle navigation for single-page web applications.
You'll need to begin your test with Elmer.Program.givenApplication
since only Elm
'application' programs can handle navigation. Provide a reference to the messages
that handle new url requests and url changes along with the view and update functions. Then
provide Elmer.Spy.use
with Elmer.Navigation.spy
so that Elmer will be able to record and process location updates by overriding
Browser.Navigation.pushUrl
and Browser.Navigation.replaceUrl
.
When you call Elmer.Program.init
you'll need to use Elmer.Navigation.fakeKey
to
give your init
function a Browser.Navigation.Key
value. Here's an example of a test
that expects the location to change when an element is clicked.
Elmer.Program.givenApplication App.OnUrlRequest App.OnUrlChange App.view App.update
|> Elmer.Spy.use [ Elmer.Navigation.spy ]
|> Elmer.Program.init (\_ -> App.init testFlags testUrl Elmer.Navigation.fakeKey)
|> Elmer.Html.target << by [ id "some-element" ]
|> Elmer.Html.Event.click
|> Elmer.Navigation.expectLocation "http://mydomain.com/funStuff.html"
You can write an expectation about the current location with Elmer.Navigation.expectLocation
.
See tests/src/Elmer/TestApps/NavigationTestApp.elm
and tests/src/Elmer/NavigationTests.elm
for
examples.
It's often necessary to describe the behavior of an application while some command is running. For example,
one might want to show a progress indicator while an HTTP request is in process. Elmer provides
general support for deferred commands. Use Elmer.Command.defer
to create a command that
will not be processed until Elmer.resolveDeferred
is called. Note that all currently
deferred commands will be resolved when this function is called.
You might want to test a command independently of any module that might use it. In that case,
use Elmer.Command.given
and provide it with a function that generates the command you
want to test. This will initiate a TestState
that simply records any messages that result when
the given command is processed. You can use the Elmer.Command.expectMessages
function to
make any expectations about the messages received. For example, here's a test that expects a
certain message when a certain command is processed:
Elmer.Command.given (\_ -> MyModule.myCommand MyTagger withSomeArgument)
|> Elmer.Command.expectMessages (\messages ->
Expect.equal [ MyTagger "Fun Result" ]
)
You can use Elmer.Command.given
with spies as it makes sense. So, you might
write a test that exercises a module with some function that needs to be stubbed (like a port command):
Elmer.Command.given (\_ -> MyModule.sendRequest MyTagger someArgument)
|> Elmer.Spy.use [ someSpy ]
|> Elmer.Command.expectMessages (\messages ->
Expect.equal [ MyTagger "Fun Result" ]
)
Using subscriptions, your application can register to be notified when certain effects occur. To describe the behavior of an application that has subscriptions, you'll need to do these things:
- Override the function that generates the subscription using
Elmer.Spy.create
along withElmer.Spy.andCallFake
and replace it with a fake subscription usingElmer.Subscription.fake
- Register the subscriptions using
Elmer.Subscription.with
- Simulate the effect you've subscribed to receive with
Elmer.Subscription.send
Here's an example test:
timeSubscriptionTest : Test
timeSubscriptionTest =
describe "when a time effect is received"
[ test "it prints the number of seconds" <|
\() ->
let
timeSpy =
Elmer.Spy.observe (\_ -> Time.every)
|> Elmer.Spy.andCallFake (\_ tagger ->
Elmer.Subscription.fake "timeEffect" tagger
)
in
Elmer.given App.defaultModel App.view App.update
|> Elmer.Spy.use [ timeSpy ]
|> Elmer.Subscription.with (\() -> App.subscriptions)
|> Elmer.Subscription.send "timeEffect" (Time.millisToPosix 3000)
|> Elmer.Html.target << by [ id "num-seconds" ]
|> Elmer.Html.expect (
Elmer.Html.Matchers.element <|
Elmer.Html.Matchers.hasText "3 seconds"
)
]
For a more complete example, check out this article.
You can manage ports during your test in just the same way you would manage any command or subscription.
Suppose you have a port that sends data to Javascript:
port module MyModule exposing (..)
port sendData : String -> Cmd msg
You can create a spy for this function just like you would for any command-generating function:
Elmer.Spy.observe (\_ -> MyModule.sendData)
|> Elmer.Spy.andCallFake (\_ -> Cmd.none)
Note that you will need to provide a fake implementation of this method since otherwise Elmer will not know how to handle the generated command.
A port that receives data from Javascript works just the same as any subscription.
port receiveData : (String -> msg) -> Sub msg
type Msg = ReceivedData String
subscriptions : Module -> Sub Msg
subscriptions model =
receiveData ReceivedData
We can create a spy for this subscription-generating function and provide a fake subscription that will allow us to send data tagged with the appropriate message during our test.
let
spy =
Elmer.Spy.observe (\_ -> MyModule.receiveData)
|> Elmer.Spy.andCallFake (\tagger ->
Elmer.Subscription.fake "fake-receive" tagger
)
in
Elmer.given MyModule.defaultModel MyModule.view MyModule.update
|> Elmer.Spy.use [ spy ]
|> Elmer.Subscription.with (\_ -> MyModule.subscriptions)
|> Elmer.Subscription.send "fake-receive" "some fake data"
|> ...
Elm uses tasks to describe asynchronous operations at a high-level. You can use Elmer to describe the behavior of applications that use the Task API. To do so:
-
Stub any task-generating functions to return a task created with
Task.succeed
orTask.fail
and the value you want as necessary for the behavior you want to describe. -
That's it.
Elmer does not know how to run any tasks other than Task.succeed
and Task.fail
.
However, Elmer does know how to properly apply all the functions from the Task API.
In this way, Elmer allows you to describe the behavior that results from operations
with tasks without actually running those tasks during your test.
Here's an example. Suppose when a button is clicked, your app creates a task that gets
the current time, formats it (using some function called formatTime : Time -> String
),
and tags the resulting string with TagFormattedTime
like so:
Time.now
|> Task.map formatTime
|> Task.perform TagFormattedTime
You can test this behavior by replacing Time.now
with a Task.succeed
that resolves to the time you want.
let
timeSpy =
Task.succeed (Time.millisToPosix 1515281017615)
|> Spy.replaceValue (\_ -> Time.now)
in
testState
|> Elmer.Spy.use [ timeSpy ]
|> Elmer.Html.target << by [ id "get-current-time" ]
|> Elmer.Html.Event.click
|> Elmer.Html.target << by [ id "current-time" ]
|> Elmer.Html.expect (
element <| hasText "1/6/2018 23:23:37"
)
See the Elmer.Task
module for more tasks that are useful when writing tests or
building extensions to Elmer.
Elmer generalizes the pattern for managing the effects of Subs
and Cmds
, allowing
you to spy on any function you like. NOTE You should use Elmer spies sparingly and
with care. Each spy that you add to your test couples that test to implementation details.
Suppose you need to write a test that expects a certain function to be called, but
you don't need to describe the resulting behavior. You can spy on a function with
Elmer.Spy.observe
and make expectations about it with Elmer.Spy.expect
.
For example, suppose you want to ensure that a component is calling a specific function in another module for parsing some string. You have tests for the parsing function itself; you just need to know that your component is using it.
parseTest : Test
parseTest =
describe "when the string is submitted"
[ test "it passes it to the parsing module" <|
\() ->
let
spy =
Elmer.Spy.observe (\_ -> MyParserModule.parse)
|> Elmer.Spy.andCallThrough
in
Elmer.given App.defaultModel App.view App.update
|> Elmer.Spy.use [ spy ]
|> Elmer.Html.target << by [ tag "input", attribute ("type", "text") ]
|> Elmer.Html.Event.input "A string to be parsed"
|> Elmer.Spy.expect (\_ -> MyParserModule.parse) (
wasCalled 1
)
]
Elmer also allows you to provide a fake implementation for any function. Suppose that you want to stub the result of the parsing function:
parseTest : Test
parseTest =
describe "when the string is submitted"
[ test "it displays the parsed result" <|
\() ->
let
spy =
Elmer.Spy.observe (\_ -> MyParserModule.parse)
|> Elmer.Spy.andCallFake (\_ ->
"Some Parsed String"
)
in
Elmer.given App.defaultModel App.view App.update
|> Elmer.Spy.use [ spy ]
|> Elmer.Html.target << by [ tag "input", attribute ("type", "text") ]
|> Elmer.Html.Event.input "A string to be parsed"
|> Elmer.Html.target << by [ id "parsing-result" ]
|> Elmer.Html.expect (Elmer.Html.element <|
Elmer.Html.Matchers.hasText "Some Parsed String
)
]
For any spy, you can make an expectation about how many times it was called like so:
Elmer.Spy.expect (\_ -> MyModule.someFunction) (wasCalled 3)
You can also expect that the spy was called with some list of arguments at least once:
Elmer.Spy.expect (\_ -> MyModule.someFunction) (
Elmer.Spy.Matchers.wasCalledWith
[ Elmer.Spy.Matchers.stringArg "someString"
, Elmer.Spy.Matchers.anyArg
, Elmer.Spy.Matcher.intArg 23
]
)
See Elmer.Spy.Matchers
for a full list of argument matchers.
Elmer.Spy.observe
is good for spying on named functions in your production code.
Sometimes, though, it would be nice to provide the code you are testing with a 'fake' function
for testing purposes only. Suppose that you are testing a module that takes a function
as an argument, and you want to expect that the function is called with a certain argument.
You can create a 'fake' function in your test module, observe it with a spy, and provide it to
the code you're testing using Elmer.Spy.inject
. For example:
someFake tagger data =
Command.fake <| tagger data
myTest =
let
spy =
Spy.observe (\_ -> someFake)
|> Spy.andCallThrough
in
Elmer.given testModel MyModule.view (MyModule.updateUsing <| Spy.inject (\_ -> someFake))
|> Elmer.Spy.use [ spy ]
|> Elmer.Html.target << by [ tag "input" ]
|> Elmer.Html.Event.input "some text"
|> Elmer.Html.target << by [ tag "button" ]
|> Elmer.Html.Event.click
|> Elmer.Spy.expect (\_ -> someFake) (
Elmer.Spy.Matchers.wasCalledWith
[ Elmer.Spy.Matchers.anyArg
, Elmer.Spy.Matchers.stringArg "some text"
]
)
Use Elmer.Spy.inject
to provide the function you want to observe so that Elmer has time to install the associated spy during the test.
Finally, you can use Spy.replaceValue
to replace the value returned by a no-argument function
(such as Time.now
) during a test. You can't make expectations about spies created in this
way; Spy.replaceValue
is just a convenient way to inject fake values during a test.
It's easy to build extensions on top of Elmer to provide custom matchers or extra functions that
help describe the behavior of an Elm application. In particular, Elmer.Value
, Elmer.Message
,
and Elmer.Effects
provide functions that are useful when writing custom matchers or extension
modules.
For a good example of an extension, see the Elmer.Http package, which adds support for describing the behavior of apps that use HTTP.
The Elmer.Http
api has been removed and moved to its own package.
Elmer.Command.resolveDeferred
has moved to Elmer.resolveDeferred
to accomodate the fact that Tasks can
also be deferred.
If you've written tests with Elmer 4.x, the Elmer.Spy
api has changed:
-
Elmer.Spy.create
is nowElmer.Spy.observe
and no longer needs a string identifier. -
When using
Elmer.Spy.observe
you must call eitherElmer.Spy.andCallThrough
orElmer.Spy.andCallFake
so that Elmer knows what to do when the observed function is called. -
Elmer.Spy.expect
now uses a reference to the observed function (rather than a string) to identify the function you'd like to assert about. -
Elmer.Spy.createWith
has been removed. Create a 'fake' function in your test module and observe it withElmer.Spy.observe
instead. -
Elmer.Spy.callable
has been removed. Instead, useElmer.Spy.inject
to provide a 'fake' function to the code under test.
If you've written tests with Elmer 3.x and plan to upgrade them to Elmer 4.0.0, here are some things you'll need to consider:
-
Elmer.Platform.Command
has changed toElmer.Command
. -
Elmer.Platform.Subscription
has changed toElmer.Subscription
. -
Elmer.Headless.givenCommand
has been replaced withElmer.Command.given
. -
Elmer.Headless.expectMessages
has been replaced withElmer.Command.expectMessages
. -
Elmer.Headless.given
has been replaced withElmer.Program.givenWorker
. -
Elmer.Http.expectThat
has been replaced withElmer.Http.expect
. -
Elmer.Http.expect
has been replaced withElmer.Http.expectRequest
. -
The
<&&>
operator has been replaced withElmer.expectAll
. -
Elmer.Html.Matchers.hasProperty
has been removed. UseElmer.Html.Matchers.hasAttribute
instead. -
Elmer.Html.target
now has a new api. Combine functions in theElmer.Html.Selector
module to build a an element selector.
To run the tests:
$ cd tests
$ ./test.sh