Two alternative Effect types for testing commands with elm-program-test

After reading Testing programs with Cmds I wonder if a simpler approach might exist.

One candidate is using double evaluation. This Ellie shows the basic idea. Rather than bundle up all the parameters so you can later create the real command or its simulated version just create both immediately and pack them together. When you want the real command you extract that from the bundle and when you want the test version you extract that. The evaluated functions store the necessary data about as well as any other type.

If these double evaluation functions are packaged to mirror their non-testable versions the way SimulatedEffect does, using this approach requires minimal refactoring. You need only replace your commands with their testable counterparts and then pipe your update function through getReal in main. The only negative I see to this approach is the slight overhead of the double evaluation but relative to the cost of actually executing the command I suspect it is insignificant.

The second approach, which avoids any runtime overhead, might be called dependency injection through conditional compilation. Basically, set up aliases so you can switch from regular commands to simulated ones by changing an import at compile time. The drawback to this is that it doesn’t look like you can do it directly in elm. If you want it automated you have to write a script to do the text substitution. A simple wrapper around elm test could be created to do the substitution prior to invocation or maybe the functionality could be added directly into elm test. The ability to inject mocks for testing might be helpful in areas besides commands.

Both techniques eliminate the need to create a custom effect type. If you have to implement either method from scratch it might be easier to create the effect type as in the original example but there doesn’t seem to be any reason to implement these yourself. They are generic. They only need implementation once and sharing. I welcome questions, comments or criticism about either of these approaches.

2 Likes

I’ve tried creating my own drop in replacements for all the elm/* modules. Besides the need for some kind of way to swap out imports at compile time (it’s not enough to just replace your own imports, you need to do it in all dependencies too) I ran into some other issues:

  1. Elm implictly adds import Platform.Cmd exposing (Cmd) to all modules. This means if you create your own Cmd type, you’ll have a name collision
  2. You get circular dependencies that make it impossible to exactly match the API of the original effect modules. For example, the type you define to represent all the possible Cmds in the Elm ecosystem will need to have something like
module Task exposing (..)

import Browser.Dom

type Task msg =
    | BrowserGetElement String (Result Browser.Dom.Error Browser.Dom.Element -> msg)
    | ...

and the Browser.Dom replacement will need to have something like this

module Browser.Dom exposing (getElement, ...)

import Task exposing (Task(..))

getElement : String -> Task Error Element
getElement = BrowserGetElement 

and now you have a circular dependency. It’s not possible to solve this by defining the types in one place and then adding aliases to all the effect modules because Browser.Dom.Error exposes all of it’s variants.

I clearly see the cyclic dependency in your example but I don’t yet have a clear understanding of why it is inevitable. I also feel uncertain if the example truly implies that cyclic dependencies are unavoidable when trying to create an easier to use version of elm-program-test.

I have a partial implementation of the interface in a current project which compiles but maybe some part of the interface can’t be added without introducing a cycle. An explanation of the current approach I’m experimenting with might help. It consists of three modules Real, Simulated, and Command. These modules consist of mostly just wrappers and aliases. Real wraps the elm Cmd stuff. Simulated wraps the SimulatedEffect stuff in elm-program-test. Command provides the means to switch between the two by changing a single import. All other modules in the project refer only to Command. Minimal examples of the three modules follow.

module Simulated exposing
    ( Body
    , Command
    , Expect
    , batch
    , httpGet
    , none
    )

import ProgramTest
import SimulatedEffect.Cmd
import SimulatedEffect.Http as Http


type alias Command msg =
    ProgramTest.SimulatedEffect msg


type alias Body =
    Http.Body


type alias Expect msg =
    Http.Expect msg


none : Command msg
none =
    SimulatedEffect.Cmd.none


batch : List (Command msg) -> Command msg
batch =
    SimulatedEffect.Cmd.batch


httpGet : { url : String, expect : Http.Expect msg } -> Command msg
httpGet =
    Http.get
module Real exposing
    ( Body
    , Command
    , Expect
    , batch
    , httpGet
    , none
    )

import Http


type alias Command msg =
    Cmd msg


type alias Body =
    Http.Body


type alias Expect msg =
    Http.Expect msg


none : Command msg
none =
    Cmd.none


batch : List (Command msg) -> Command msg
batch =
    Cmd.batch


httpGet : { url : String, expect : Http.Expect msg } -> Command msg
httpGet =
    Http.get
module Command exposing
    ( Body
    , Command
    , Expect
    , batch
    , httpGet
    , none
    )

{- Toggle these imports to switch between production and testing. -}

import Real as Base



-- import Simulated as Base


type alias Body =
    Base.Body


type alias Command msg =
    Base.Command msg


type alias Expect msg =
    Base.Expect msg


none : Command msg
none =
    Base.none


batch : List (Command msg) -> Command msg
batch =
    Base.batch


httpGet : { url : String, expect : Base.Expect msg } -> Command msg
httpGet =
    Base.httpGet

So, it isn’t a drop in replacement in the sense that I can just add a line like import Command as Cmd and be done. However, I might call it a one-to-one replacement in that I just have to make some text substitutions like replace Http.get with Command.httpGet. The compiler and type system, as always, help locating missed changes. If I’m starting a new project neither method seems to add any significant overhead.

The Real and Simulated modules could be packaged up or incorporated into elm-program-test so that they are installable like any other package. It appears the Command module is something you have to basically cut and paste into your code. We need to either make a substitution in this file to switch between Real and Simulated or do something like keep separate versions of it in the production and testing branches of our repo.

You point out that to do something like this:

…you need to do it in all dependencies too

I agree. I remember the frustration of implementing just the handful of functions I needed and uncovering dependency after dependency that had to be met first. But, I eventually got to the bottom of it. If others think this is a worthwhile approach then it can be done once and shared so everyone doesn’t have to go through the same frustration. It’s mostly just a lot of boilerplate that the compiler guides you through. @avh4 has already done the tough work.

I agree it isn’t an ideal solution but it seems preferable in some if not most cases compared to the method currently suggested in the elm-program-test example. Both versions require you to replace your Cmd types with a different type. In the example version you have to write your own type that includes logic to bundle up your parameters. In the version I propose you only need to copy a file into your project. It also comes with the small benefit that it gets compiled away in production.

I don’t know if either proposed solution qualifies as good but I think they both might meet the standard of better and I’m curious to hear what others think or discussing ways to improve these approaches.

1 Like

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