How to test an Elm module without exposing everything? - elm

Working my way through "Elm in Action", I understand that to write tests, all functions and types needed in a test suite for some module must be exposed by that module. This seems to break encapsulation. I don't want to expose internal functions and type constructors that should remain hidden just to make them testable. Is there a way to expose internal functions an types for testing only, but not for regular use?

There are a few strategies to tackle this problem, each with its pluses and minuses. As a running example, let's make a module that models a simple store on the server, that we would like to test the internals of:
module FooService exposing (Foo, all, update)
import Http
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
type Id
= Id String
type alias Foo =
{ id : Id
, title : String
}
apiBase : String
apiBase =
"https://example.com/api/v2"
all : (Result Http.Error (List Foo) -> msg) -> Cmd msg
all tagger =
Http.get
{ url = apiBase ++ "/foos"
, expect = Http.expectJson tagger decodeMany
}
update : Foo -> (Result Http.Error Foo -> msg) -> Cmd msg
update foo tagger =
Http.post
{ url = apiBase ++ "/foos/" ++ idToString foo.id
, body = foo |> encode |> Http.jsonBody
, expect = Http.expectJson tagger decode
}
idToString : Id -> String
idToString (Id id_) =
id_
encode : Foo -> Value
encode foo =
Encode.object
[ ( "id", Encode.string (idToString foo.id) )
, ( "title", Encode.string foo.title )
]
decode : Decoder Foo
decode =
Decode.map2 Foo
(Decode.field "id" (Decode.map Id Decode.string))
(Decode.field "title" Decode.string)
decodeMany : Decoder (List Foo)
decodeMany =
Decode.field "values" (Decode.list decode)
Note that as is, the module is ideally encapsulated, but utterly untestable. Let's look at some strategies to alleviate this problem:
1. Tests within modules
elm-test is not actually that prescriptive about where you put your tests, as long as there is an exposed value with the Test type.
As such you can do something like this:
module FooService exposing (Foo, all, update, testSuite)
-- FooService remains exactly the same, but the following is added
import Test
import Fuzz
testSuite : Test
testSuite =
Test.describe "FooService internals"
[ Test.fuzz (Fuzz.map2 Fuzz (Fuzz.map Id Fuzz.string) Fuzz.string) "Encoding roundtrips"
\foo ->
encode foo
|> Decode.decodeValue decoder
|> Expect.equal (Ok foo)
-- more tests here
]
This can be quite nice in the sense that tests are also collocated with the functions they are testing. The downside is that modules can get quite large with all the test code in them. It also requires you to move elm-test from your test dependencies into the runtime dependencies. This should theoretically not have any runtime impact, since elm's dead code elimination is excellent, but it does leave a lot of developers a little nervous.
2. Internal Modules
Another option, which is heavily used within elm packages (since there is direct support for that kind of hiding in the built in elm.json) is to have modules that are considered internal to a certain module or library and no other modules should read from them. This can be enforced by convention or I believe there are elm-review rules on can use to enforce these boundaries.
In our example it would look something like this:
module FooService.Internal exposing (Foo, Id(..), encode, decode, decodeMany, idToString)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
type Id
= Id String
type alias Foo =
{ id : Id
, title : String
}
idToString : Id -> String
idToString (Id id_) =
id_
encode : Foo -> Value
encode foo =
Encode.object
[ ( "id", Encode.string (idToString foo.id) )
, ( "title", Encode.string foo.title )
]
decode : Decoder Foo
decode =
Decode.map2 Foo
(Decode.field "id" (Decode.map Id Decode.string))
(Decode.field "title" Decode.string)
decodeMany : Decoder (List Foo)
decodeMany =
Decode.field "values" (Decode.list decode)
then FooService would simply become:
module FooService exposing (Foo, all, update)
import Http
import FooService.Internal as Internal
type alias Foo =
Internal.Foo
apiBase : String
apiBase =
"https://example.com/api/v2"
all : (Result Http.Error (List Foo) -> msg) -> Cmd msg
all tagger =
Http.get
{ url = apiBase ++ "/foos"
, expect = Http.expectJson tagger Internal.decodeMany
}
update : Foo -> (Result Http.Error Foo -> msg) -> Cmd msg
update foo tagger =
Http.post
{ url = apiBase ++ "/foos/" ++ Internal.idToString foo.id
, body = foo |> Internal.encode |> Http.jsonBody
, expect = Http.expectJson tagger Internal.decode
}
then all tests can be written against the internal module.
As I said, this is an extremely common pattern you will see in the majority of published elm packages, but in applications it suffers a bit from the fact that the tooling support isn't quite as good. For instance autocomplete will offer you these internal functions even in modules that shouldn't have access to them.
Nonetheless, we use this pattern quite successfully at work.
3. Change the design
Perhaps if a module isn't testable, then it is doing too much. One can look into things like the effect pattern to change the design to be more testable. For instance, one could argue that performing HTTP requests is outside the core competency of dealing with Foos, and the boundary should be at the decoder/encoder stage, which would make it quite testable; then a central module would deal with Http communication centrally.
We've been looking in this direction for a bit, but haven't found a good way to make it nice with really complex server interactions, but it might be something worth thinking about in each individual case: why is this module not testable? Would an alternative design be just as good and also be testable?

Related

TYPE MISMATCH - This function cannot handle the argument sent through the (|>) pipe:

I am a super elm begginer and trying to make app.
Currently I am struggling to make landing page and http request to a server.
But, I am stuck here...
I have init function something like this below.
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
Model key TopPage
|> goTo (Route.parse url)
The definition of my Model is below.
-- MODEL
type alias Model =
{ key : Nav.Key
, page : Page
, name : String
, tags : List Tag
, jwt : String }
and, goTo function is below.
goTo : Maybe Route -> Model -> ( Model, Cmd Msg )
goTo maybeRoute model =
case maybeRoute of
Nothing ->
( { model | page = NotFound }, Cmd.none )
Just Route.Top ->
( { model | page = TopPage }, Cmd.none )
...
type Route is below.
type Route
= Top
| User String
| Repo String String
parse : Url -> Maybe Route
parse url =
Url.Parser.parse parser url
parser : Parser (Route -> a) a
parser =
oneOf
[ map Top top
, map User string
, map Repo (string </> string)
]
but following error has occured.
-- TYPE MISMATCH -------------------------------------------------- src/Main.elm
This function cannot handle the argument sent through the (|>) pipe:
54| Model key TopPage
55| |> goTo (Route.parse url)
^^^^^^^^^^^^^^^^^^^^^
The argument is:
String -> List Tag -> String -> Model
But (|>) is piping it a function that expects:
Model
What did I make mistake here?....
Your Model type has five fields, but in the line
Model key TopPage
you are only providing values for the first two of the five. You are missing values for the name, tags and jwt fields. Provide values for these and the problem should go away.
When you declare a type alias such as Model, Elm creates a constructor function also named Model. Elm functions support partial application, in that if you pass in values for some but not all of the arguments, you end up with a function that takes in the rest of the values. You provided two arguments, so you end up with a function that takes three arguments and returns a Model.
There are two ways of building a value of a type. Given a simple example of a Person type alias:
type alias Person = { name : String, age : Int }
You can construct a value by specifying all fields (note that you don't have to specify Person in the constructor; Elm's compiler is smart enough to know it by its shape):
jane : Person
jane = { name = "Jane", age = 35 }
Or you can build a value by using the type name and specify each field's values in the order in which they were defined. In this style, you can think of Person acting like a function with two parameters that returns a Person value.
jane : Person
jane = Person "Jane" 35
In each case, you have to specify all fields of the type when you construct it in order to obtain a complete Person value. However, that is not the complete story. It is possible to leave off the age parameter when constructing a Person, but the result isn't a Person, it's a function that takes an age and returns a Person. In other words,
janeAged : Int -> Person
janeAged = Person "Jane"
You can strip off as many parameters from the end as you'd like to make more variations on that constructor, even stripping out all parameters:
somebody : String -> Int -> Person
somebody = Person
Back to your example. You are constructing a Model value by only specifying two parameters (Model key TopPage). The value of that expression does not result in a Model, but in a function that takes three more parameters to create a Model. And that's why the error message indicated you need three parameters to construct a model.
You need to specify all values of Model when creating it.

Command not being invoked after calling update

Building on the Elm navigation tutorial, I needed to execute a command to fetch additional data once navigating to my CategoryRoute.
My View.elm looks something like this:
view : Model -> Html Msg
view model =
div []
[ page model ]
page : Model -> Html Msg
page model =
case model.categories of
RemoteData.Success categories ->
case model.currentRoute of
CategoryListRoute ->
CategoryList.view categories
CategoryRoute id ->
let maybeCategory =
categories
|> SubCategories
|> flatten
|> filter (\category -> category.id == id)
|> head
_ = update (OnCategorySelected id) model
in
case maybeCategory of
Just category ->
Category.view category
Nothing ->
notFound
You'll notice that I'm calling update with the OnCategorySelected message myself when the currentRoute changes to the CategoryRoute.
My Update.eml looks something like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnArticlesFetch response ->
let
_ = log "got response" response
in
( { model | articles = response }, Cmd.none)
OnLocationChange location ->
let
newRoute =
parseLocation location
in
( { model | currentRoute = newRoute }, Cmd.none )
OnCategorySelected id ->
( model, (getArticles model.tenant id) )
And finally, my Commands.eml looks like so:
getArticles : String -> String -> Cmd Msg
getArticles tenant id =
let
url =
"https://" ++ tenant ++ ".my.api"
_ = log "getArticles for " id
in
Http.post url (Http.jsonBody (encoder id)) decoder
|> RemoteData.sendRequest
|> Cmd.map OnArticlesFetch
I was expecting that once I'll call update OnCategorySelected, it will in turn invoke the getArticles function, which is passed a Cmd Msg, which I had thought will be invoked once the response comes in.
The problem I'm facing is that while update OnCategorySelected and getArticles seem to get invoked (as indicated by the log printouts log "getArticles for " id), I'm seeing no outgoing HTTP calls, no errors, no results and no log "got response" response printouts.
I'm confused as to what am I doing wrong here and what's the pattern for actually fetching more data as one navigates to a page in Elm...
Elm is a pure language where side effects are relegated to the framework. Calling the update function does not actually perform any work itself. It simply returns a value that can be handed off to the Elm framework directing it to interact with the outside world. That means when you call update from within the page function and discard the result, nothing happens.
One thing that can cause confusion is that Debug.log actually does get called and prints to the console, which violates the aforementioned purity of the language. It's just a magic function which exists only for debugging so hopefully it doesn't cause too much confusion.
You should instead be handling the RemoteData.Success case in the update function after parsing the route in the OnLocationChange case and returning a result which includes the getArticles result Cmd.
OnLocationChange location ->
let
newRoute =
parseLocation location
cmd =
case newRoute of
CategoryRoute id ->
getArticles model.tenant id
_ ->
Cmd.none
in
( { model | currentRoute = newRoute }, cmd )

Objects into JSON decoder through Elm ports

I am passing an array of objects via ports into my Elm app. An example of an one of the objects in the array is:
{
FullName: 'Foo Bar',
Location: 'Here'
}
As you can see the keys in the object start with a capital, so I need to decode these in Elm. In my Elm code I have a type for the Person
type alias Person =
{ fullName : String
, location : String
}
and the port:
port getPeople : (List Json.Decode.Value -> msg) -> Sub msg
Finally I have a decoder (I am using Elm Decode Pipeline) to parse the data into the Person type.
peopleDecoder : Decoder Person
peopleDecoder =
decode Person
|> required "FullName" string
|> required "Location" string
My question is how do I map the incoming port data into the Person type? I know I could do this in JS but I'd rather do it in my Elm code.
Json.Decode.decodeValue can decode a Json.Decode.Value, but it returns a Result String (List Person).
If you defined your Msg like this:
type Msg
= GetPeople (Result String (List Person))
You could set up your subscription like this:
port getPeople : (Json.Decode.Value -> msg) -> Sub msg
subscriptions : Model -> Sub Msg
subscriptions model =
getPeople (GetPeople << decodeValue (list peopleDecoder))
(Note that the first argument in the port has been changed to just a Value instead of List Value)

What is a good way to internationalize an Elm application?

I need to internationalize the UI strings in my ELM HTML application to 3 different languages.
I am thinking of doing this:
1) I will get the currentLanguage from Javascript, and pass it down in the ProgramWithFlags. I'llkeep the language in the model
2) I'll setup some types in my code
type alias Languages = English | French | Spanish
-- One of these for each string I want to internationalize
type alias InternationalizedStrings = StringHello | StringFoo | StringBar
3) I'll make a function for returning each translated phrase to use in my Views.
getPhrase: InternationalizationString Languages -> string
getPhrase stringId lang =
case lang of
English ->
case stringId of
StringHello -> "Hello"
StringFoo -> "Foo"
StringBar -> "Bar"
French ->
case stringId of
StringHello -> "Bonjour"
StringFoo -> "Oui"
StringBar -> "Non"
...
Is there a better way to do this? I have lots of string.
In case you want compiler errors when you don't provide the translation for a string your solution is on the right track.
If you want to either allow yet untranslated strings or find it tedious to have a type for every translatable string, you might want to switch to a Dict-based solution. to tinker with it, just throw it into http://elm-lang.org/try:
import Dict exposing (Dict)
import Html exposing (text)
type Language
= English
| French
| Spanish
type alias Key =
String
main =
text <| translate French "Hello"
translate : Language -> Key -> String
translate lang key =
let
dict =
case lang of
English ->
Dict.fromList
[ ( "Hello", "in english" )
]
French ->
Dict.fromList
[ ( "Hello", "salut" )
]
Spanish ->
Dict.fromList
[ ( "Hello", "hola" )
, ( "someKeyThatOnlyExistsInSpanish", "42" )
]
in
Dict.get key dict |> Maybe.withDefault ("can not find translation for " ++ key)
A while ago, I had a crack at internationalisation, and came up with the following setup:
define the language in a global model
have a very simple function, to be used in view modules and functions
the function has a signature of localString : Language -> String -> String
localString basically does a lookup in a global dictionary to find a translation from the word you provide to the language you provide.
it will always give back a String, defaulting to the original word, if it cannot find the word you provide, or if it cannot find the translation to the language you provide.
keep the global dictionary (and helper) functions NOT in the model, but in a separate file (it is pretty static data, which won't change in runtime).
the Language type is a Union Type, to ensure we only have 'approved' languages.
the actual dictionary uses conversions to string. The Dict type does not allow strong types as the key.
That way, using internationalisation has minimal impact on the rest of the code:
You need to add a Language to your Model (which you could get through JS port)
You can still use short and readable code in your views to translate, like
p [] [ text <| localString model.language "car" ]
All hardcoded strings in your own code remain in one simple default language, to keep the rest of your code readable.
Here is the gist of what I was working on, you can copy/ paste to elm-lang.org/try (not fully tested functionally or performance-wise with large numbers of strings and translations)
import Html exposing (div, p, text)
import Dict exposing (Dict)
-- Manage your languages below
type Language = English | Spanish | French
defaultLanguage : Language
defaultLanguage = English
languageToKey : Language -> LanguageKey
languageToKey language =
case language of
English -> "English"
Spanish -> "Spanish"
French -> "French"
keyToLanguage : LanguageKey -> Language
keyToLanguage key =
case key of
"English" -> English
"Spanish"-> Spanish
"French" -> French
_ -> defaultLanguage
english : LocalWord -> (Language, LocalWord)
english word =
(English, word)
spanish : LocalWord -> (Language, LocalWord)
spanish word =
(Spanish, word)
french : LocalWord -> (Language, LocalWord)
french word =
(French, word)
-- Internal stuff
type alias Word = String
type alias LocalWord = String
type alias LanguageKey = String
type alias Dictionary = Dict Word WordDict
type alias WordDict = Dict LanguageKey LocalWord
init : Dictionary
init =
Dict.fromList []
newLocalWord : Word -> (Language, LocalWord) -> Maybe WordDict -> Maybe WordDict
newLocalWord word (localLanguage, localWord) wordDict =
wordDict
|> Maybe.withDefault (Dict.fromList [])
|> Dict.insert (languageToKey defaultLanguage) word
|> Dict.insert (languageToKey localLanguage) localWord
|> Just
addTranslation : Word -> (Language, LocalWord) -> Dictionary -> Dictionary
addTranslation word newTranslation dictionary =
dictionary
|> Dict.update word (newLocalWord word newTranslation)
localString : Language -> Word -> LocalWord
localString language word =
let
wordEntry =
Dict.get word globalDictionary
localLanguage =
languageToKey language
in
case wordEntry of
Just wordDict ->
Dict.get localLanguage wordDict
|> Maybe.withDefault word
Nothing ->
word
add : Word -> List (Language, LocalWord) -> Dictionary -> Dictionary
add word translationList dictionary =
List.foldl (addTranslation word) dictionary translationList
-- BUILD DICTIONARY BELOW
globalDictionary : Dictionary
globalDictionary =
init
|> add "Hello" [ spanish "Hola", french "Bonjour" ]
|> add "Man" [ spanish "Hombre", french "Homme" ]
|> add "Child" [ french "Enfant" ]
-- For Elm-lang Try only
localModel =
{ language = Spanish }
main =
div []
[ p []
[ text <| "Hello in Spanish: "
++ localString localModel.language "Hello"
]
, p []
[ text <| "In dictionary, but not in Spanish: "
++ localString localModel.language "Child"
]
, p []
[ text <| "Is not in dictionary: "
++ localString localModel.language "Car"
]
]
I wrote a blog post about this a couple months ago. If you have the ability, try to prefer using ADTs over Dicts since Dicts can't give you the same guarantees at a type level (which is why Dict.get returns Maybe a). ADTs can also have the data type you're acting on type checked as well MyPhrase Int String that you can pattern match on and use whatever toString method you'd like (e.g. MyPhrase foo bar -> "My phrase contains " ++ toString foo ++ " & " ++ bar ++ "."). That being said, existing systems/translation services might make it difficult to use this method without writing a parser from .elm to .json or .po.

In Elm what is the correct way to implement my own toString

In Elm what is the correct way to take my Model and implement a toString function?
The type I am looking for would be toString : Model -> String, I am able to make a similar function with the type of toStr : Model -> String but I would think I would want the function to be called toString.
Example program (the Coin Changer kata):
module CoinChanger where
import Html exposing (..)
import StartApp.Simple as StartApp
import Signal exposing (Address)
import Html.Attributes exposing (..)
import Html.Events exposing (on, targetValue)
import String
---- MAIN ----
main =
StartApp.start
{
model = emptyModel
,update = update
,view = view
}
---- Model ----
type alias Model =
{
change : List Int
}
emptyModel : Model
emptyModel =
{
change = []
}
---- VIEW ----
toStr : Model -> String
toStr model =
model.change
|> List.map (\coin -> (toString coin) ++ "¢")
|> String.join ", "
view : Address String -> Model -> Html
view address model =
div []
[
input
[
placeholder "amount to make change for"
, on "input" targetValue (Signal.message address)
, autofocus True
-- style
]
[]
, div []
[
text (toStr model)
]
]
---- UPDATE ----
changeFor : Int -> List Int
changeFor amount =
[ 25, 10, 5, 1 ]
|> List.foldl
(\coin (change, amount)
-> ( change ++ List.repeat (amount // coin) coin
, amount % coin)
)
([], amount)
|> fst
update : String -> Model -> Model
update change model =
{ model | change =
case String.toInt change of
Ok amount
-> changeFor amount
Err msg
-> []
}
I would think the correct way to do this would be to call the function toString, but that gives me the following error from the compiler:
Detected errors in 1 module.
-- TYPE MISMATCH ----------------------------------------------- CoinChanger.elm
The type annotation for toString does not match its definition.
42│ toString : Model -> String
^^^^^^^^^^^^^^^ The type annotation is saying:
{ change : List Int } -> String
But I am inferring that the definition has this type:
{ change : List { change : List Int } } -> String
Renaming the function to toStr (or something not called toString) fixes the issue but seems wrong. What is the correct way to do this?
The problem is that, calling your function toString, you are overriding the toString function of the Basics module, which you are using at line 45.
To avoid this, you'll need to import the Basics module and use Basics.toString instead of simply toString to eliminare the ambiguity
The accepted answer is well out of date for anyone writing Elm 0.19+. The current solution is to write your own toString function for the type you want converted. There is a Debug.toString for use during development but its use in your code will prevent building for production.