Advice on growing and maintaining a unit test suite for a SPA?

Looking for advice on writing and maintaining a good unit test suite. I’ve been following the guidance given in Life of a File on how to grow my codebase, but that leaves me wondering how best to approach unit tests in a disciplined manner. I’ve tried just writing them to exercise the code I’ve already written (so like development driven testing), but I often find that it’s hard to maintain them with all the big refactors that seem so common.

To be more specific, here are a few pain points I’ve noticed:

  • I find it really hard to pull out good data structures that will stay stable enough to build modules and tests around.
  • The compiler is already checking so much that my tests often feel redundant or unnecessary.
  • The Elm runtime does something that I can’t stub or mock. E.g. https://github.com/elm-explorations/test/issues/24
  • The little helper functions that are easy to test aren’t exposed by my modules, so my tests can’t access them.

For now I’ve just been focusing on keeping my integration tests up to date using Cypress, and not worrying so much about unit tests but I miss having the feedback loop you get from a good unit test suite.

Has anyone had success growing a unit test suite alongside their codebase? How did you make it worth the extra time and effort? Any tips or examples I could look at? Thanks!

3 Likes

I experienced similar pain points around testing when I first started with Elm a few years ago. In particular, I found it hard to write tests that gave me freedom to refactor (I found myself just unit testing each particular function), and I had trouble describing the behavior of my software at a high level due to difficulties around opaque types used by various libraries, including Cmd and Sub values.

I ended up writing my own testing tool called Elmer which works with elm-test. It was fun to do and ended up teaching me a lot about testing; I’ve used it on some personal projects, and my colleagues have used it at work. Give it a try! (Buyer beware, though, it’s a personal project and I can’t guarantee I’ll maintain it indefinitely)

Here’s an example of a simple game I developed while practicing TDD with Elmer. You could check out its test suite: https://github.com/brian-watkins/mindmaster

Also, Elmer itself was developed using TDD and has an extensive test suite you could check out. It saved me from breaking some behavior with a new feature just the other day!

I’m a big fan of TDD so I’d be excited to see the Elm community embrace more tools that enable this style of development.

2 Likes

Can you please add your tool to https://github.com/isRuslan/awesome-elm might be helpful some day :slight_smile:

Nice! That looks like just what I was looking for. Will give it a shot! :+1:

Also, adding it to awesome-elm sounds like a good idea because I don’t know if I would have ever found it on my own.

Im not 100% sure its a good idea over all, but I have effectively organized my projects, so that the tests are inside the module they are meant to test.

module Data.Size exposing (Size, tests)

type alias Size = { width : Int, height : Int }

-- Helpers here

-- TESTS --

tests : Test
tests =

I tried this out explicitly to avoid having to expose internal helper functions like you also want to avoid.

Then to make it all work, there needs to be a main test file that imports all your project modules, just like your Main.elm file would.

There are straightforward workarounds to not being able to test Cmds or Browser.Keys.

For Cmds, I’ve made an Effect type, and returned ( Model, Effect ) from my update functions and then a effectToCmd : Effect -> Cmd Msg function to apply as a final step outside the update function. I know its a just work around, but I feel like this approach came with a lot of unexpected extra nice-ness.

For Browser.Navigation.Key Ive had good luck with what @andys8 recommended in that thread. You can just make your own key type like…

module Navigation.Key exposing (Key(..), pushUrl)

import Browser.Navigation as Nav

type Key
    = Key Nav.Key
    | TestKey

pushUrl : Key -> String -> Cmd msg
pushUrl key url =
    case key of
        Key navKey ->
            Nav.pushUrl navKey url

        TestKey ->
            Cmd.none

Then in your test, use the TestKey to build whatever data you need to make.

5 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.