Livebook で doctest

## はじめに

Livebook で doctest を試してみました

@nako_sleep_9h さんが以下の記事に書いてくれていたのですが、まだ試していなかったので

https://qiita.com/nako_sleep_9h/items/ce7edaf7167d599cb734

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/doctests.livemd

:::note warn
2023/7/14 Livebook 0.10.0 がリリースされ、 doctest が Live Doctests にパワーアップしました

最新版は以下を参照

https://qiita.com/RyoWakabayashi/items/05622a8d279fc0d85ee8
:::

## 実行環境

Livebook 0.9.3

## doctest とは

@ma2ge　さんの記事にまとめてありました

https://qiita.com/ma2ge/items/b6b26335ecc1b2181897

これを参考に、 Livebook に doctest を書いていきます

## シンプルな例

Livebook で新規ノートブックを開き、セルに以下のコードを入力します

:::note info
特に依存モジュールがないのでセットアップセルは使いません
:::

```elixir
defmodule OKTest do

  @doc """
  Add two numbers

  ## Parameters

    - a: a number
    - b: a number

  ## Examples

  iex> OKTest.add(1, 1)
  2

  iex> OKTest.add(1.1, 2)
  3.1

  iex> OKTest.add("1", 2)
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  @spec add(number(), number()) :: number()
  def add(a, b) do
    a + b
  end
end
```

この中の `iex>` で始まっている行にテストの実行内容を記述し、次の行に想定される出力を書きます

例えば `OKTest.add(1, 1)` の実行結果は `2` になると想定しています

実行すると、以下のような実行結果が表示されます

![スクリーンショット 2023-06-24 19.46.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d315f974-6ae9-eaee-ffc0-ca2ea777a34b.png)

`3 doctests, 0 failures` の部分が doctest の結果です

次は失敗するケースを作りましょう

```elixir
defmodule NGTest do

  @doc """
  Add two numbers

  ## Parameters

    - a: a number
    - b: a number

  ## Examples

  iex> NGTest.add(1, 1)
  3

  iex> NGTest.add(1.1, 2)
  3.3

  iex> NGTest.add("1", 2)
  ** (RuntimeError) error!!
  """
  @spec add(number(), number()) :: number()
  def add(a, b) do
    a + b
  end
end
```

実行すると、セル自体は `Evaluated` で正常実行できましたが、 doctest の結果が `3 doctests, 3 failures` で赤くなっています

![スクリーンショット 2023-06-24 19.47.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/41af7502-3ceb-466e-ad4d-5660e33e53cb.png)

ちなみに、 doctest の結果全文は以下のようになっています

```
3 doctests, 3 failures

1) doctest NGTest.add/2 (line 13)
   doctest:
     iex> NGTest.add(1, 1)
     3
   expected:
     3
   got:
     2

2) doctest NGTest.add/2 (line 16)
   doctest:
     iex> NGTest.add(1.1, 2)
     3.3
   expected:
     3.3
   got:
     3.1

3) doctest NGTest.add/2 (line 19)
   expected exception RuntimeError but got ArithmeticError with message "bad argument in arithmetic expression"
   doctest:
     iex> NGTest.add("1", 2)
     ** (RuntimeError) "error!!"
```

どのテストで、どう想定と違っていたのか分かるので便利ですね

## 入力が複数行の場合

入力が複数行の場合、少し注意が必要です

```elixir
defmodule MultiLine do

  @doc ~S"""
  Split text lines

  ## Parameters

    - str: text

  ## Examples

  iex> MultiLine.split("a\nb")
  ["a", "b"]

  iex> ~s(a
  ...>b
  ...>c)
  ...>|> MultiLine.split()
  ["a", "b", "c"]
  """
  @spec split(String.t()) :: list(String.t())
  def split(str) do
    str
    |> String.trim()
    |> String.split("\n")
  end
end
```

1番目のテストは `MultiLine.split("a\nb")` というように、改行コード `\n` を書くパターンです

`"""` で囲っている中に `\n` のエスケープを入れるため、ドキュメントの先頭を `@doc ~S"""` というように、 `~S` のシギルに変更しています

:::note warn
`~S` を使わないと、 `iex> MultiLine.split("a` でテストコード自体が改行していると見なされ、エラーになります
:::

2番目のテストは `~s` を使って複数行に書いたパターンです

doctest で複数行を入力にする場合、2行目以降の先頭に `...>` を付加します

## 出力が複数行の場合

出力が複数行の場合も同様、 `~S` を使います

```elixir
defmodule Words do

  @doc ~S"""
  Join words into multiline

  ## Parameters

    - words: list of words

  ## Examples

  iex> Words.join(["a", "b"])
  "a\nb"

  iex> Words.join(["a", "b", "c"])
  ~s(a
  b
  c)
  """
  @spec join(list(String.t())) :: String.t()
  def join(words) do
    Enum.join(words, "\n")
  end
end
```

1番目のパターンは `\n` で改行を表したパターンです

2番目のパターンは `~s` で出力を複数行に書いたパターンです

## まとめ

Livebook 上でモジュールをテストしたいときに便利ですね