<!--
  canonical-url: https://www.avonture.be/fr/blog/bats-unit-tests
  published:     2025-07-10
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Linux - Scripts Bash - Lancer des tests unitaires avec bats/bats

> Découvrez comment tester unitairement vos scripts Bash sous Linux avec le puissant framework bats (Bash Automated Testing System). Exemples concrets d'assertions, de vérifications de sortie et d'échecs.

---
![Linux - Scripts Bash - Lancer des tests unitaires avec bats/bats](/img/v2/bash.webp)

**TL;DR**

Cet article propose une introduction complète aux tests unitaires de scripts Bash avec le framework `bats-core`. Il montre comment démarrer rapidement en lançant les tests dans un container Docker, sans configuration compliquée. Le guide couvre l'écriture de tests de base, l'utilisation des différents types d'assertions comme `assert_output`, `assert_failure` et `assert_equal`, et montre comment structurer des tests pour de vraies fonctions shell. Des sujets plus avancés comme le mocking de commandes, le test de sorties multilignes et colorées, la vérification du contenu de fichiers et l'usage des fonctions `setup`/`teardown` pour les fixtures sont également expliqués avec des exemples concrets.

Comme tous les développeurs qui vivent en ligne de commande, j'écris des scripts Bash sous Linux. Comme tout programmeur, je suis censé écrire des tests unitaires. Bon, je dois avouer que je n'en écris que rarement.

Il y a quelque temps, j'ai écrit un ensemble de scripts Bash qui forment une bibliothèque de fonctions (comme un framework) et là, l'intérêt d'avoir des tests unitaires est encore plus grand puisque ces fonctions sont censées être stables et servir de fondations à des scripts plus avancés. *Deux habitudes rendent une telle bibliothèque testable dès le départ : [limiter le nombre de paramètres des fonctions](/blog/linux-bash-too-many-function-parameters) et [documenter chaque fonction avec un bloc de doc](/blog/linux-generate-documentation-from-bash-scripts).*

Dans cet article, nous voyons comment écrire des tests unitaires pour des scripts Bash. Si vous cherchez l'équivalent en PHP, voyez [Write PHP unit tests using Pest](/blog/pest_tips) ; pour des tests fonctionnels / de bout en bout plutôt que des tests unitaires, j'ai aussi parlé de Behat, [Cypress](/blog/cypress) et [des tests navigateur de Pest v4](/blog/pest-functional-testing) ; et si vous préférez qu'on vous écrive une première version de la suite de tests, [un LLM local peut générer les cas manquants](/blog/ollama-test-generator).

## Commençons par une petite mise en bouche {#lets-start-with-a-little-hors-doeuvre}

Comme toujours, créons d'abord un nouveau dossier pour jouer avec quelques exemples.

Lancez `mkdir -p /tmp/bats && cd $_` pour créer un dossier temporaire et y entrer ; puis lancez `code .` pour démarrer VSCode et ouvrir le dossier.

Nous allons créer un fichier d'illustration simple, appelons-le `tests/simple.bats`, et copier/coller le contenu suivant :

**tests/simple.bats**

```bats
#!/usr/bin/env bats

setup() {
    bats_load_library bats-support
    bats_load_library bats-assert
}

@test "Asserting it's hello" {
  run echo "hello"
  assert_output "hello"
  assert_success
}

@test "Simple.bats exists" {
  run ls simple.bats
  assert_output --regexp "[Ss]imple\.bats"
  assert_success
}
```

Et maintenant, la partie très difficile est, ah non, en fait vraiment facile, de lancer [Bats-core](https://bats-core.readthedocs.io/en/stable/) :

```bash
$ docker run --rm -it -w /code/tests -v .:/code bats/bats:latest simple.bats
```

Et... ça marche.

![Test simple](./images/simple_test.webp)

### Qu'avons-nous fait ? {#what-did-we-do}

Nous avons créé un exemple très basique avec deux vérifications.

La première s'appelle *Asserting it's hello* et nous avons lancé `echo "hello"` exactement comme dans un script shell. Ensuite, en lançant `assert_output "hello"`, nous vérifions que la sortie de la commande lancée est bien `hello` et c'est un succès (donc c'est `hello` et rien d'autre).

C'est fait avec ce script :

**tests/simple.bats**

```bats
@test "Asserting it's hello" {
  run echo "hello"
  assert_output "hello"
  assert_success
}
```

Et la seconde vérification s'appelle *Simple.bats exists* : nous faisons simplement un `ls simple.bats` et vérifions si, pour l'illustration, `Simple.bats` ou `simple.bats` est retourné dans la liste des fichiers (ceci, pour illustrer l'usage d'une expression régulière).

**tests/simple.bats**

```bats
@test "Simple.bats exists" {
  run ls simple.bats
  assert_output --regexp "[Ss]imple\.bats"
  assert_success
}
```

### Vérifier un échec {#asserting-a-failure}

Bien sûr, nous pouvons aussi vérifier un échec. Modifiez votre fichier `simple.bats` comme ceci :

**tests/simple.bats**

```bats
#!/usr/bin/env bats

setup() {
    bats_load_library bats-support
    bats_load_library bats-assert
}

@test "Asserting it's hello" {
  run echo "hello"
  assert_output "hello"
  assert_success
}

@test "Simple.bats exists" {
  run ls simple.bats
  assert_output --regexp "[Ss]imple\.bats"
  assert_success
}

# highlight-start
@test "Removing an inexistent file" {
  run rm INEXISTENT_FILE
  assert_failure
}
# highlight-end
```

Donc, dans la nouvelle fonction de test, nous essayons simplement de supprimer un fichier inexistant et nous attendons, forcément, un échec :

![Vérifier un échec](./images/assert_failure.webp)

## Quelques cas d'usage réels {#some-real-world-use-cases}

Imaginez l'arborescence simplifiée suivante :

```tree expanded=true showJSX=false debug=false title=""
.
├── src
│   └── assert.sh
└── tests
    ├── assert.bats
```

Le fichier `src/assert.sh` contient le code shell Linux que vous voulez tester. Vos scénarios de test doivent être stockés dans le dossier `tests`. Puisque nous allons écrire des tests pour le fichier `src/assert.sh`, créons `tests/assert.bats`.

Ci-dessous le contenu de `src/assert.sh`. Commençons par une fonction simple : vérifier si un binaire donné est installé sur le système ou non. Le nom du binaire doit être passé en paramètre à la fonction.

**src/assert.sh**

```bash
#!/usr/bin/env bash

function assert::notEmpty() {
    local -r variable="${1:-}"
    local -r errorMessage="${2:-"The variable can't be empty."}"

    [[ -z ${variable} ]] && echo "${errorMessage}" >&2

    return 0
}

function assert::binaryExists() {
    local -r binary="${1:-}"

    assert::notEmpty "${binary}" \
        "You should specify the name of the program whose existence needs to be checked."

    if [[ -z "$(command -v "$binary" || true)" ]]; then
        echo "The ${binary} binary wasn't found on your system." >&2
        exit 1
    fi

    return 0
}
```

Ci-dessous le contenu de `tests/assert.bats`.

**tests/assert.bats**

```bats
#!/usr/bin/env bats

setup() {
    bats_load_library bats-assert
    bats_load_library bats-support

    source ./../src/assert.sh

    # Create a dummy file every time a test function is called
    tmpFile="/tmp/bats/assert.tmp"
    [[ ! -d "/tmp/bats" ]] && mkdir "/tmp/bats"

    touch "${tmpFile}"
}

# Once the test function has been called, remove the temporary
teardown() {
    [[ -f "${tmpFile}" ]] && rm -f "${tmpFile}"
    return 0
}

@test "assert::binaryExists - Assert some binaries exists" {
    # clear is a native Linux command
    run assert::binaryExists "clear"

    # We expect thus binaryExists return a zero exit code (=success)
    assert_success

    # and no output message since it's successful
    assert_output ""


    # Another test with the native ls command
    run assert::binaryExists "ls"
    assert_output ""
    assert_success
}

@test "assert::binaryExists - Assert some binaries didn't exist" {
    run assert::binaryExists "fakeProgram"

    # Since fakeProgram didn't exist, we expect binaryExists exit with code 1
    assert_failure 1

    # We also expect this, exact, error message
    assert_output "The fakeProgram binary wasn't found on your system."
}
```

La ligne de commande :

```bash
$ docker run --rm -it -w /code/tests -v .:/code bats/bats:latest assert.bats
```

En la lançant, nous attendons un succès pour binaryExists avec les commandes `clear` et `ls` (puisqu'elles sont bien installées sur notre système) et nous attendons un échec pour `fakeProgram` mais, puisque nous utilisons `assert_failure`, notre scénario de tests devrait passer.

Ajoutons une vérification pour voir si une image Docker spécifique existe déjà ou non sur le système :

**src/assert.sh**

```bash
#!/usr/bin/env bash

function assert::notEmpty() {
    local -r variable="${1:-}"
    local -r errorMessage="${2:-"The variable can't be empty."}"

    [[ -z ${variable} ]] && echo "${errorMessage}" >&2

    return 0
}

function assert::binaryExists() {
    local -r binary="${1:-}"

    assert::notEmpty "${binary}" \
        "You should specify the name of the program whose existence needs to be checked."

    if [[ -z "$(command -v "$binary" || true)" ]]; then
        echo "The ${binary} binary wasn't found on your system." >&2
        exit 1
    fi

    return 0
}

# highlight-start
function assert::dockerImageExists() {
    local -r imageName="${1:-}"
    local -r errorMessage="${2:-The Docker image \"$imageName\" did not exist}"

    assert::notEmpty "${imageName}" "You should specify the name of the Docker image whose existence needs to be checked."

    # When the image exists, "docker images -q" will return his ID (f.i. `5eed474112e9`), an empty string otherwise
    if [[ "$(docker images -q "$imageName" 2> /dev/null)" == "" ]]; then
        echo "${errorMessage}" >&2
        exit 1
    fi

    return 0
}
# highlight-end
```

et le fichier `tests/assert.bats` mis à jour :

**tests/assert.bats**

```bats
#!/usr/bin/env bats

setup() {
    bats_load_library bats-assert
    bats_load_library bats-support

    source ./../src/assert.sh

    # Create a dummy file every time a test function is called
    tmpFile="/tmp/bats/assert.tmp"
    [[ ! -d "/tmp/bats" ]] && mkdir "/tmp/bats"

    touch "${tmpFile}"
}

# Once the test function has been called, remove the temporary
teardown() {
    [[ -f "${tmpFile}" ]] && rm -f "${tmpFile}"
    return 0
}

@test "assert::binaryExists - Assert some binaries exists" {
    # clear is a native Linux command
    run assert::binaryExists "clear"

    # We expect thus binaryExists return a zero exit code (=success)
    assert_success

    # and no output message since it's successful
    assert_output ""


    # Another test with the native ls command
    run assert::binaryExists "ls"
    assert_output ""
    assert_success
}

@test "assert::binaryExists - Assert some binaries didn't exist" {
    run assert::binaryExists "FakeProgram"

    # Since FakeProgram didn't exist, we expect binaryExists exit with code 1
    assert_failure 1

    # We also expect this, exact, error message
    assert_output "The FakeProgram binary wasn't found on your system."
}

# highlight-start
@test "assert::dockerImageExists - Assert docker image exists - using mockup" {
    # Mock - we'll create a very simple docker override and return a fake ID
    # This will simulate the `docker images -q "AN_IMAGE_NAME"` which return
    # the ID of the image when found
    docker() {
        echo "feb5d9fea6a5"
    }

    run assert::dockerImageExists "A-great-Docker-Image"
    assert_output ""
    assert_success
}

@test "assert::dockerImageExists - Assert docker image didn't exist - using mockup" {
    # Mock - we'll create a very simple docker override and return a fake ID
    # This will simulate the `docker images -q "AN_IMAGE_NAME"` which return
    # the ID of the image when found; here return an empty string to simulate
    # a non-existent image
    docker() {
        echo ""
    }

    run assert::dockerImageExists "Fake/image" "ERROR - Bad choice, that image didn't exist"
    assert_failure 1
    assert_output --partial "Bad choice, that image didn't exist"
}
# highlight-end
```

## Plus d'exemples {#more-examples}

### assert\_equal {#assert\_equal}

Vérifie simplement que les deux valeurs sont égales. Ici, nous appelons une fonction qui retourne la longueur d'un tableau et nous vérifions que c'est la valeur attendue.

**tests/simple.bats**

```bats
@test "array::length - Calculate the length of an array" {
    arr=("one" "two" "three" "four" "five")

    assert_equal $(array::length arr) 5
}

function array::length() {
    local -n array_length=$1
    echo ${#array_length[@]}
}
```

### assert\_failure {#assert\_failure}

`assert::binaryExists` fera un exit 1 si le binaire ne peut pas être trouvé. Un message d'erreur du type *The binary can't be found* sera affiché sur la console.

**tests/simple.bats**

```bats
@test "Assert binary didn't exist" {
    # Simulate which and return an error meaning "No, that binary didn't exist on the host"
    which() {
        exit 1
    }

    run assert::binaryExists "Inexistent_Binary" "Ouch, no, no, that binary didn't exist on the system"

    assert_failure

    assert_output --partial "Ouch, no, no, that binary didn't exist on the system"
}

function assert::binaryExists() {
    local binary="${1}"
    local msg="${2:-${FUNCNAME[0]} - File \"$binary\" did not exist}"

    [[ -z "$(which "$binary" || true)" ]] && echo "$msg" && exit 1

    return 0
}
```

### assert\_output {#assert\_output}

`assert_output` a deux options bien pratiques : `--partial` et `--regexp`.

Utiliser `--partial` permet par exemple de vérifier que la sortie contient un texte brut donné, comme une phrase précise dans un écran d'aide.

**tests/simple.bats**

```bats
@test "Show the help screen" {
    source 'src/helpers.sh'

    arrArguments=("--help")

    run helpers::showHelp ${arrArguments[@]}

    assert_output --partial "Convert a revealJs slideshow to a PDF document"
}
```

Utiliser `--regexp` permet d'employer une expression régulière :

**tests/simple.bats**

```bats
@test "Show the help screen" {
    source 'src/helpers.sh'
    arrArguments=("--input InvalidFile")
    run helpers::__process ${arrArguments[@]}
    assert_output --regexp "ERROR - The input file .* doesn't exist."
}
```

### assert\_success {#assert\_success}

`assert::binaryExists` retourne 0 quand le binaire est trouvé. La fonction s'exécute en silence (aucune sortie).

**tests/simple.bats**

```bats
@test "Assert binary exists" {
    run assert::binaryExists "clear" # clear is a native Linux command
    assert_output ""                 # No output when success
    assert_success
}

function assert::binaryExists() {
    local binary="${1}"
    local msg="${2:-${FUNCNAME[0]} - File \"$binary\" did not exist}"

    [[ -z "$(which "$binary" || true)" ]] && echo "$msg" && exit 1

    return 0
}
```

### Vérifier les couleurs ANSI {#check-for-ansi-colors}

Imaginez le code suivant :

**script.sh**

```bash
__RED=31

function console::printRed() {
    for line in "$@"; do
        printf "\e[1;${__RED}m%s\e[0m\n" "$line"
    done
}
```

Nous voulons vérifier que la ligne sera affichée en rouge.

**tests/simple.bats**

```bats
@test "console::printRed - The sentence should be displayed" {
    run console::printRed "This line should be echoed in Red"
    assert_output "�[1;31mThis line should be echoed in Red�[0m"
    assert_success
}
```

### Vérifier une sortie multiligne {#check-for-multi-line-output}

Imaginez le code suivant :

**script.sh**

```bash
function console::banner() {
    printf "%s\n" "============================================"
    printf "= %-40s =\n" "$@"
    printf "%s\n" "============================================"
}
```

Cela va écrire trois lignes sur la console, comme par exemple :

```bash
# ============================================
# = Step 1 - Initialisation                  =
# ============================================
```

Pour vérifier plusieurs lignes, utilisez le tableau `$lines` comme ceci :

**tests/simple.bats**

```bats
@test "console::banner - The sentence should be displayed" {
    run console::banner "Step 1 - Initialisation"

    assert_equal "${lines[0]}" "============================================"
    assert_equal "${lines[1]}" "= Step 1 - Initialisation                  ="
    assert_equal "${lines[2]}" "============================================"

    assert_success
}
```

### Vérifier par rapport à un fichier {#check-against-a-file}

Imaginez une fonction qui va parser un fichier et, par exemple, supprimer certains paragraphes. Nous devons vérifier que le contenu est correct, une fois la fonction lancée.

Pour cela, imaginez une fonction `removeTopOfFileComments`. La fonction va parser le fichier et supprimer les commentaires HTML (`<!-- ... -->`) présents en haut du fichier.

Pour le test, nous allons créer un fichier avec trois lignes vides, puis un bloc de commentaire HTML, puis deux lignes vides, puis le code HTML. Donc, en supprimant le commentaire HTML, nous aurons cinq lignes vides suivies du bloc HTML : nous devons donc vérifier que notre fichier contient six lignes.

L'astuce utilisée est :

* `cat --show-ends --show-tabs "$tempfile"` c'est-à-dire récupérer le contenu du fichier mais avec un `$` là où il y a un saut de ligne et, ici, aussi `^I` pour les tabulations.
* ensuite nous passons le résultat dans `tr "\n" "#"` : au lieu d'obtenir six lignes, nous n'en obtenons qu'une, en remplaçant les sauts de ligne par `#`.

Et voilà, puisque nous avons une variable avec une seule ligne (dans notre exemple : `$#$#$#$#$#<html><body/></html>$#`), nous pouvons la comparer à ce que nous attendons :

**tests/simple.bats**

```bats
@test "html::removeTopOfFileComments - remove HTML comments - with empty lines" {
    tempfile="$(mktemp)"

    # Here, we'll have extra, empty, lines. They should be removed too
    echo '' >$tempfile
    echo '' >>$tempfile
    echo '' >>$tempfile
    echo '<!--  ' >>$tempfile # We also add extra spaces before the start tag
    echo '   Lorem ipsum dolor sit amet, consectetur adipiscing elit.' >>$tempfile
    echo '   Morbi interdum elit a nisi facilisis pulvinar.' >>$tempfile
    echo '   Vestibulum fermentum consequat suscipit. Vestibulum id sapien metus.' >>$tempfile
    echo '-->     ' >>$tempfile # We also add extra spaces after the end tag
    echo '' >>$tempfile
    echo '' >>$tempfile
    echo '<html><body/></html>' >>$tempfile

    run html::removeTopOfFileComments "$tempfile"

    # Get now the content of the file
    #   We expect three empty lines (the three first)
    #       The HTML comment has been removed
    #   Then there are two more empty line (so we'll five empty lines)
    #   And we'll have our "<html><body/></html>" block.
    #
    #   cat --show-ends --show-tabs will show the dollar sign (end-of-line) and f.i. ^I for tabulations
    #   tr "\n" "#" will then convert the line feed character to a hash so, in fact, fileContent will
    #   be a string like `$#$#$#$#$#<html><body/></html>$#`
    fileContent="$(cat --show-ends --show-tabs "$tempfile" | tr "\n" "#")"

    # Once we've our string, compare the fileContent with our expectation
    assert_equal "$fileContent" "\$#\$#\$#\$#\$#<html><body/></html>\$#"
}
```

### Vérifier un fichier avec une regex {#check-against-a-file-using-a-regex}

Un second scénario peut être : vous avez une fonction d'écriture (pensez à un fichier de log) et vous voulez vérifier la présence d'une ligne donnée dans le fichier.

L'exemple ci-dessous s'appuie sur `bats-file` et sa méthode `assert_file_contains`. Cette méthode demande un nom de fichier et un motif regex.

**tests/simple.bats**

```bats
setup() {
    bats_load_library bats-support
    bats_load_library bats-assert
    bats_load_library bats-file

    #! "grep" without the "-P" argument seems to not support repetition like "\d{4}"
    #
    # a date like `2022-04-07`
    regexDate="[0-9][0-9][0-9][0-9]\-[0-9][0-9]\-[0-9][0-9]"
    # a time like `17:41:22`
    regexTime="[0-9][0-9]\:[0-9][0-9]\:[0-9][0-9]"
    # a time zone difference like "0200"
    regexUTC="[0-9]*" #! Should be [0-9][0-9][0-9][0-9] but didn't work???

    # The final pattern so we can match f.i. `[2022-04-07T18:00:20+0200] `
    __DATE_PATTERN="\[${regexDate}\T${regexTime}\+${regexUTC}.*\]\s"

    return 0
}

@test "log::write - Write a line in the log" {
    local sentence=""
    sentence="This is my important message"
    run write "${sentence}"

    assert_file_exist "/tmp/bats_log.tmp"

    echo "${__DATE_PATTERN}${sentence}" >/tmp/regex.tmp
    assert_file_contains "/tmp/bats_log.tmp" "${__DATE_PATTERN}${sentence}"
    assert_success
}
```

### Vérifier qu'une valeur n'est PAS dans un fichier {#check-that-a-value-is-not-in-a-file}

Un autre usage de assert\_failure peut être de lancer une commande comme un grep et de s'attendre à obtenir une erreur :

```bash
run grep "REGEX_SOMETHING_THAT_SHOULD_BE_MISSING" "/tmp/test.log"
assert_failure 1
```

### Quelques fonctions spéciales {#some-special-functions}

#### setup {#setup}

La fonction `setup` est appelée avant de lancer un test. Pour chaque fonction `@test` présente dans le scénario, la fonction `setup()` sera appelée.

Dans l'exemple suivant, comme il y a deux fonctions de test, `setup()` sera appelée deux fois.

**tests/simple.bats**

```bats
setup() {
    bats_load_library bats-support
    bats_load_library bats-assert

    source 'src/env.sh'
}

@test "env::assertFileExists - Assert .env file exists - The path isn't initialised" {
    run env::assertFileExists
    assert_failure
}

@test "env::assertFileExists - Assert .env file exists - The file exists" {
    ENV_ROOT_DIR="/tmp"
    ENV_FILENAME=".env.bats.testing"
    touch ${ENV_ROOT_DIR}/${ENV_FILENAME}
    run env::assertFileExists
    assert_success
}
```

#### teardown {#teardown}

Tout comme `setup`, la fonction `teardown` sera appelée pour chaque test mais une fois le test lancé. C'est le bon endroit pour, par exemple, supprimer des fichiers créés pendant l'exécution d'un test.

```bash
teardown() {
    rm -f /tmp/bats
}
```

### Lancer les tests {#running-the-tests}

La commande `docker run --rm -it -w /code/tests -v .:/code bats/bats:latest simple.bats` va lancer le fichier `simple.bats` tandis que `docker run --rm -it -w /code/tests -v .:/code bats/bats:latest` va lancer tous les fichiers `.bats` du répertoire de travail.

#### Mocking {#mocking}

Nous pouvons surcharger une fonction pendant un test. Prenez le cas d'usage suivant : nous avons une fonction qui retourne 0 quand une image Docker donnée est présente sur l'host. La fonction retourne 1 et affiche une erreur sur la console si l'image n'est pas trouvée.

**script.sh**

```bash
function assert::dockerImageExists() {
    local image="${1}"
    local msg="${2:-The Docker image \"$image\" did not exist}"

    # When the image exists, "docker images -q" will return his ID (f.i. `5eed474112e9`), an empty string otherwise
    [[ "$(docker images -q "$image" 2>/dev/null)" == "" ]] && echo "$msg" && exit 1

    return 0
}
```

Nous devons donc surcharger la réponse de docker. Quand l'image est censée être là, il suffit de retourner une chaîne non vide, n'importe quoi sauf une chaîne vide. Retournons un faux ID pour vraiment simuler la réponse de `docker images -q`.

**tests/simple.bats**

```bats
@test "Assert docker image exists" {
    # Mock - we'll create a very simple docker override and return a fake ID
    # This will simulate the `docker images -q "AN_IMAGE_NAME"` which return
    # the ID of the image when found
    docker() {
        echo "feb5d9fea6a5"
    }

    source assert.sh
    run assert::dockerImageExists "A-great-Docker-Image"
    assert_output "" # No output when success
    assert_success
}
```

Et retournons une chaîne vide pour simuler une image inexistante.

**tests/simple.bats**

```bats
@test "Assert docker image didn't exist" {
    # Mock - we'll create a very simple docker override and return a fake ID
    # This will simulate the `docker images -q "AN_IMAGE_NAME"` which return
    # the ID of the image when found; here return an empty string to simulate
    # an inexistent image
    docker() {
        echo ""
    }

    source assert.sh
    run assert::dockerImageExists "Fake/image" "Bad choice, that image didn't exist"
    assert_output --partial "Bad choice, that image didn't exist"
    assert_failure
}
```

## Conclusion {#conclusion}

Bats apporte à Bash ce qui lui a toujours manqué : un moyen de prouver qu'un script fait encore ce qu'il faisait le mois dernier. Le coût de mise en place est proche de zéro — un fichier `.bats`, l'image `bats/bats`, et aucun runner à installer — et le bénéfice apparaît la première fois qu'un refactoring casse une fonction que vous aviez oubliée.

Commencez petit. Prenez le script que vous avez le plus peur de toucher, écrivez trois tests pour son cas nominal, et ajoutez-en un chaque fois que vous y corrigez un bug. C'est généralement suffisant pour transformer un fichier Bash que vous n'osiez plus modifier en un fichier que vous pouvez modifier.
