Not exposing internals hurts testability

Hi,

I’m trying to practise TDD with Elm and encapsulate implementation as much as possible. I find it hard to manage both properly and I looking for some advice on how to handle this properly.

Where I struggle in particular is creating an expected result for my test. I cannot construct them in my test because that would mean I have to expose the implementation. I currently fix this by creating builder helpers in my module, but I don’t like that for the following reasons:

  1. I’m not sure that the builder method constructs my expected result properly, because there’s no way of testing it
  2. The implementation is leaking out of the module through the builder methods. It’s now also possible to create the internals through a way that wouldn’t be there if I didn’t use tests.

I believe in both TDD and encapsulation but currently I feel I cannot use them properly in Elm. Any advise in how I could make the best of both?

PS. I’m working on an app, not on a package. This is perhaps relevant for the advice.

8 Likes

Have you tried moving the parts needed by tests to internal module? Then you could expose them internally for tests while not exposing them to users of your package.

p.s. I’m still learning myself how best to handle cases like this.

1 Like

Hi malaire,

Thank you for your swift reply.

Do you mean moving tests into the implementation module? If so, I have considered it, but did not went forward with this approach, because I’d like to keep those separate due to different responsibilities.

If not, I completly misunderstood your reply and hope you can elaborate a bit more on this.

No, I meant like this:

-- src/MyPackage/Internal.elm
module MyPackage.Internal exposing (MyType(..), builder)
type MyType = MyType ...

builder : ...

-- src/MyPackage.elm
module MyPackage exposing (MyType)
import MyPackage.Internal as Internal exposing (MyType(..))
type alias MyType = Internal.MyType

-- tests/Tests.elm
module Tests exposing (..)
import MyPackage
import MyPackage.Internal as Internal

Package itself exposes only module MyPackage so MyType variants and builder are only available internally within the package, and for tests.

1 Like

This clearifies it indeed.

Unfortunatly I’m not building a package on it’s own, but want to prevent exposing the internals of my own modules in my own app. In that case, src/MyPackage/Internal would be exposed anyway to all my other modules in the app, right?

If I’m correct, do you’ve any other advice? :slight_smile:

Test only the public interface of the module. Whatever is private should remain private.

Your builder functions are black boxes. You are not supposed to check whatever they construct if this is not permitted by the exposed interface. You are supposed to check behavior of the black boxes and this behavior should be exposed through the API.

Attempting to test whatever is not exposed will most likely lead to breaking encapsulation.

Think about it like this: If you cannot prove the error exists without opening the box, then what exactly is the error? If you can prove the error exists without opening the box, then you don’t need to open the box.

LATER EDIT: an example of permitting the testing of the black box is this:

someFunction : SomeOutsideType -> BlackBox 

reverseFunction : BlackBox -> SomeOutsideType

The first function is a builder, a function that creates the private type. The second function is something that permits the testing of the builder (at least in part).

9 Likes

Is your input data coming from an api? And is your result then built internally through a Decoder? e.g:

module Post exposing (Post, decoder)

import Json.Decode as Decode exposing (Decoder)


type Post
    = Post Post_


type alias Post_ =
    { id : String
    , title : String
    }


decoder : Decoder Post
decoder =
    Decode.map Post decoder_


decoder_ : Decoder Post_
decoder_ =
    Decode.map2 Post_
        (Decode.field "id" Decode.string)
        (Decode.field "title" Decode.string)

One way of working with this (without exposing any builders or internals) is to decode a JSON string in your tests using the decoder and do any assertions on the data from there, maybe something like:

suite : Test
suite =
    describe "Post"
        [ Test.fixture "Has some interesting property" postFixture <|
            \posts ->
                ...do some assertions on the decoded post
                
        
        ]


postFixture : Result Decode.Error Post
postFixture = Decode.decodeString Post.decoder """
    { "id": "12345", "title": "some title" }
"""

If you can generate api responses this works really nicely. Ignore this though if this isn’t your problem :sweat_smile:

1 Like

It’s also possible to put Test values in the source file, right next to the function you want to test. Then you can test anything not exposed. Run your tests with elm-test src tests or similar.

3 Likes

Wanted to echo this :sunny:

4 Likes

I like to put my tests inside the module of the stuff they mean to test, giving my tests full access to the internal implementation details without also exposing those internal implementation details.

module Data.Foo exposing (Foo, f, tests)

import Test exposing (Test)
import Expect


type Foo 
f : Foo -> Int
g : (Foo -> Foo) -> List Foo

tests : Test
tests = Test.describe "Foo Tests" 
    [  Test.test "g test" <| \_ ->
    -- ..
    ]
3 Likes

First of all, thanks for the advice. The reverse testing was put away in my head and it’s a good refreshment :slight_smile:

I agree. That’s also what I’m trying to do. I guess it’s best to share an example to express my challenge more clearly. So this is basically the structure I use:

import OtherModule exposing (OtherType)

type alias Model = { a: String, b: Int }

type SomeType = 
     SomeType Model

doCmd : SomeType -> Cmd msg
doCmd (SomeType someType) = 
    -- Do something with someType

parse : OtherType -> SomeType
parse : otherType
    -- Transform OtherType into SomeType

In this case I would like to expose only ‘SomeType’ and the methods ‘doCmd’ and ‘parse’ to force every logic arround SomeType to take place in this module

The challenge I have, there’s no way of testing the ‘parse’ method because I cannot construct a SomeType for an expected result outside this module.

Does this make my challenge more clear?

This is something I also thought of. I must admit I don’t like it, because I see testing and implementation as seperate things and I would like to keep them apart.

I hope there’s another solution.

I often encounter this need when I’m trying to nail down a complex function. Usually, I have an idea about how to split it into simpler parts, and I start by writing those simpler parts. The problem arises when I try the TDD approach. I need to write tests first, but the complex function is nowhere near ready yet, and the simpler function isn’t supposed to be a part of the exposed interface. So what am I supposed to write tests for?

I begin by exposing the internal function and writing tests for it. Then I get those tests passing, do the refactoring etc. à la usual TDD. I rinse and repeat for other simple parts of the complex function, and then I compose the actual exposed complex function from those parts, again using TDD.

The trick at that last step is reusing the tests for the internal parts to test the top level function instead. If it defines a promise of the final API, it gets converted. If it just tests the internal interface, it goes away, having served its purpose.

Then I’m left with just the tests for the final top-level complex function, which hopefully cover most of the cases it’s expected to work in. If there are any more cases left to cover, I write them and tweak the code as needed to get them passing.

At that point, I can safely un-expose the internal functions and just test the public API. Even if the API needs to be upgraded later, there’s usually no need to expose the internals again, as I already have working code. Even new internal functions usually don’t have to be exposed, as their contribution can be tested through the working public API.

I see this approach as a sort of work-in-progress “scaffolding”, to be removed once the API can stand on its own feet.

5 Likes

To be sure I understand you: Taking above example, in the beginnning you would expose Model, and test the ‘parse’ function. If it works you would throw the test away and hide Model again?

Yes, I think. Which part of this would be the final exposed API?

Testing language-level opaque structures like Cmd is another pain point entirely :slight_smile:

Since parse isn’t something a user of your module can use in a meaningful way on it’s own, you can’t test it in isolation.

Only the composition of parse >> doCmd (with type OtherType -> Cmd msg) is meaningful for a user, so that’s what you should test.

In your case this is made a bit more difficult by the fact that doCmd returns a Cmd msg, which isn’t really testable. You might consider returning something else, e.g. an exposed custom type that stands for all the possible cases. E.g. something like type OutCmd = Cmd1 | Cmd2 | ...

2 Likes

<rant/intrested-in-this>: As someone not into TDD but agreeing on its promise this is always what gets me. If I can only test public API I can only have tests for a module returning “Some expected test result” for a given input but if I want to test the actual code I would like tests for the functions involved. Now I can only test the whole not the details. As in I dont want my algorithm to be publicly used but I would like to make sure my algorithm works as I expect it to. If only allowed to test from the highest level there are so much more that can break/make it harder to debug than just call that function and verify it does the right thing. It sort of feels as the case for unit tests vs. integration tests. As in “integration tests are worthless, you get no info on what went wrong and they are slow/have too many concerns”. I feel you are sort of in integration test mode when only testing public API (for a complicated module) instead of writing a test for calculateFractalAnomaly: Int -> Float -> Array Byte you need to setup the dependant thing generating the `Array Byte´ and do a whole dance around converting strings to the data and so on. When developing should you write tests for the internal functions and throw those away when you are done or what? I saw some post a while ago where you could add the tests to your module which allows you to test internal stuff. My immediate reaction was that this is the correct way. What if Elm would have that feature in core?

1 Like

What you should do is write tests using the public interface with setups that would ensure each branch in the internal implementation is reached.

These are still unit tests, integration tests are at a higher level. The unit in this case is the module and its exposed interface.

1 Like

I get that. But the thing discussed is that that is not always reasonable right?

What I meant with throwing away (and the comparison to integration tests) was that starting a project you start with the internal stuff maybe and you do it using TDD. Later you arrive at what should be public api and then you have clear tests that you need to rewrite using your public api but that means you need to pull in other stuff/make it more involved just to setup the tests since the public api takes xml-data for example so now you need an xml library to test your thing and you end up further from the actual thing you are testing and add more points of failure. Bad example I know but trying to make the point. :slight_smile: In a lot of cases I feel it would be valuable to keep those tests that are clear and to the point.

Is this something that is considered a big NONO in TDD community or are there different camps?

I have in the past worked with obj-c/swift and there you can expose stuff only for the test-target.

Something like this in Elm maybe:

module Mikaxyz.UiWidget exposing 
    (view
    , Attributes(..)
    , someFunction for Tests
    , SomeType for Tests
    )
1 Like

I just think there is yet no clean and elm-like way in elm, to do tdd. I like your idea of exposing internals for tests though :+1:t2:

I think testing the public interface is okay and sufficient so far, but if you want complete coverage, or develop the tdd-way from the very beginning of each module (which I also wanted to do at some point and realized, it’s not possible ) you get stuck.

Here you have two interesting talks, the second one approaches testing very differently. So maybe this edge is a point where Elm could develop a nice solution for itself and it’s tooling…

Writing testable Elm:

Creating tests from types: