通过“Elm in Action”,我了解到要编写测试,某个模块的测试套件中所需的所有函数和类型都必须由该模块公开。这似乎打破了封装。我不想公开应该保持隐藏的内部函数和类型构造函数,只是为了使它们可测试。有没有办法将内部函数公开类型仅用于测试,但不用于常规使用?
1 回答
有一些策略可以解决这个问题,每种策略都有其优点和缺点。作为一个运行示例,让我们创建一个模块,在服务器上模拟一个简单的商店,我们想测试它的内部:
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)
请注意,该模块是理想封装的,但完全无法测试。让我们看一些缓解这个问题的策略:
1. 模块内的测试
elm-test 实际上并没有规定您将测试放在哪里,只要该Test
类型有一个公开的值。
因此,您可以执行以下操作:
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
]
从某种意义上说,这可能非常好,因为测试也与它们正在测试的功能并置。缺点是模块可能会变得非常大,其中包含所有测试代码。它还要求您将 elm-test 从测试依赖项移至运行时依赖项。这在理论上应该不会对运行时产生任何影响,因为 elm 的死代码消除非常出色,但它确实让很多开发人员有点紧张。
2. 内部模块
在 elm 包中大量使用的另一个选项(因为在内置 elm.json 中直接支持这种隐藏)是具有被认为是某个模块或库内部的模块,并且不应读取其他模块他们。这可以通过约定强制执行,或者我相信有 elm-review 规则可以用来强制执行这些边界。
在我们的示例中,它看起来像这样:
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)
那么 FooService 将简单地变成:
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
}
然后可以针对内部模块编写所有测试。
正如我所说,这是您将在大多数已发布的 elm 包中看到的极其常见的模式,但在应用程序中,它会受到工具支持不太好的事实的影响。例如,即使在不应访问它们的模块中,自动完成功能也会为您提供这些内部功能。
尽管如此,我们在工作中还是非常成功地使用了这种模式。
3.改变设计
也许如果一个模块是不可测试的,那么它做的太多了。人们可以研究诸如效果模式之类的东西来改变设计,使其更具可测试性。例如,有人可能会争辩说,执行 HTTP 请求超出了处理 Foos 的核心能力,边界应该在解码器/编码器阶段,这将使其非常可测试;然后一个中央模块将集中处理 Http 通信。
我们已经在这个方向上寻找了一段时间,但还没有找到一种很好的方法来使它与真正复杂的服务器交互变得更好,但在每个单独的情况下可能都值得考虑:为什么这个模块不可测试?替代设计会同样好并且可测试吗?