Search code examples
testingelmdebouncingelm-test

Testing debouncing in Elm


I'm attempting to test a debouncing function in my Elm application and can't figure out how.

The debouncing is applied to a text field for fuzzy search to avoid making too many http requests, it is modelled on this example https://ellie-app.com/jNmstCdv3va1 and follows the same logic.

type alias Model =
    { search : Maybe String 
    , searchResult : List User
    , debouncingCounter : Int
    }

init : Model
init = 
    { search = Nothing
    , searchResult = [] 
    , debouncingCounter = 0
    }

debounceTime : Time
debounceTime = 350 * Time.millisecond

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of 

        (...)

        SearchInput search ->
            let 
                newCounter = model.debouncingCounter + 1
            in
            case search o
                "" -> ({model | search = Nothing, searchResult = []}, Cmd.none)
            _ -> 
                ({ model | search = Just search, debouncingCounter = newCounter }
                , Process.sleep debounceTime |> Task.perform (always (Timeout newCounter)))

        Timeout int ->
            if int==model.debouncingCounter then
                (update SendSearch {model | debouncingCounter = 0 })
            else 
                (update NoOperation model)

        SendSearch ->
            case model.search of 
                Nothing -> 
                    (model, Cmd.none)
                Just string -> 
                    let 
                        cmd = Http.send ReSendSearch <| postApiAdminUserSearchByQuery string
                    in
                    (model, cmd)

        ReSendSearch result ->
            case result of 
                Err _ -> 
                    (model, Cmd.none)

                Ok usersList -> 
                    ({model | searchResult = usersList}, Cmd.none )

I want to ensure that, after calling

update (searchInput "string") init

the Http request is only sent after the debounceTime.

I can easily test the model right after the update function is called with the searchInput message. For instance, here I check that the initial value of the "debouncingCounter" field in the model gets set to 1:

startDebounce : Test
startDebounce =
test "debouncingCounter is set to 1 after search input is updated" <|
    \_ ->
        Users.init
            |> Users.update (Users.SearchInput "abc") 
            |> Tuple.first
            |> .debouncingCounter
            |> Expect.equal 1

However, I don't see how I would be able to test the effects of the delayed Cmd Msg on the model since I can't directly apply the cmd value returned by the update function.

Process.sleep debounceTime |> Task.perform (always (Timeout newCounter))

It seems that different ways of implementing debouncing won't solve the problem as they all rely on command messages.


Solution

  • Depending on what exactly you want to test, you might follow different approaches.

    If you want to test

    1. Your code on SearchInput returning the proper command: you may consider using elm-testable.

    2. The elm runtime executing the Process.sleep command properly: this will fall into the integration/end-to-end test scenarios. So, you will need to test the complete compiled app, using one of the end-to-end/JS tools.

    3. Your code processing the Timeout x messages properly: just write a separate test case for that.