Bash и тестирование

При написании более чем тривиального script в bash, я часто задаюсь вопросом, как сделать код проверяемым.

Как правило, трудно написать тесты для кода bash из-за того, что на функциях, которые принимают значение, и возвращают значение, и на высоких функциях, которые проверяют и устанавливают некоторый аспект в среде, файловая система, вызывать программу и т.д. - функции, зависящие от среды или имеющие побочные эффекты. Таким образом, установочный и тестовый коды становятся намного сложнее, чем те, которые они тестируют.


Например, рассмотрим простую функцию для проверки:

function add_to_file() {
  local f=$1
  cat >> $f
  sort -u $f -o $f
}

Тестовый код для этой функции может состоять из:

add_to_file.before:

foo
bar
baz

add_to_file.after:

bar
baz
foo
qux

И тестовый код:

function test_add_to_file() {
   cp add_to_file.{before,tmp}
   add_to_file add_to_file.tmp
   cmp add_to_file.{tmp,after} && echo pass || echo fail
   rm add_to_file.tmp
}

Здесь 5 строк кода проверяются 6 строками тестового кода и 7 строками данных.


Теперь рассмотрим несколько более сложный случай:

function distribute() {
   local file=$1 ; shift
   local hosts=( "[email protected]" )
   for host in "${hosts[@]}" ; do
     rsync -ae ssh $file $host:$file
   done
}

Я даже не могу сказать, как начать писать тест для этого...


Итак, есть ли хороший способ сделать TDD в сценариях bash, или я должен отказаться и приложить свои усилия в другом месте?

Ответ 1

Итак, вот что я узнал:

  1. Тем не менее, есть некоторые тестовые среды, написанные на bash и для bash...

  2. Дело не только в том, что Bash не подходит для TDD (хотя некоторые другие языки приходят на ум лучше подходят), но в типичных задачах, для которых используется Bash (установка, настройка системы), для которых сложно писать тесты для И в особенности сложно настроить тест.

  3. Недостаточная поддержка структуры данных в Bash затрудняет отделение логики от побочных эффектов, и, действительно, в скриптах Bash обычно мало логики. Это затрудняет разбиение сценариев на тестируемые фрагменты. Есть некоторые функции, которые можно протестировать, но это исключение, а не правило.

  4. Функция - это хорошая вещь (тм), но они могут зайти так далеко.

  5. Вложенные функции могут быть даже лучше, но они также ограничены.

  6. В конце концов, с большими усилиями можно получить некоторую информацию, но она протестирует менее интересную часть кода и сохранит большую часть тестирования как хорошее (или плохое) старое ручное тестирование.

Мета: Я решил ответить (и принять) свой собственный вопрос, потому что я не мог выбирать между ответами Синан Юнюр (проголосовал за) и Мувициелем (проголосовал), которые были одинаково полезны и проницательны. Хочу отметить ответ Стефано Борини, что, хотя поначалу он меня не впечатлил, я научился ценить его с течением времени. Также были полезны его шаблоны проектирования или лучшие практики для ответов на сценарии оболочки, которые были упомянуты выше.

Ответ 2

Если вы одновременно пишете код с помощью тестов, постарайтесь сделать это на функциях, которые не используют ничего, кроме их параметров, и не изменяют среду. То есть, если ваша функция может также работать в подоболочке, тогда ее будет легко протестировать. Он принимает некоторые аргументы и выводит что-то в stdout или в файл, или, может быть, что-то делает в системе, но вызывающий не чувствует побочных эффектов.

Да, у вас будет большая цепочка функций, передающая некоторую переменную WORKING_DIR, которая также может быть глобальной, но это незначительное неудобство по сравнению с задачей отслеживания того, что каждая функция читает и модифицирует. Включение модульных тестов - это просто бесплатный бонус.

Попробуйте минимизировать случаи, когда вам нужен выход. Небольшое злоупотребление подоболочкой будет идти длинным путем, чтобы держать вещи красиво отделенными (за счет производительности).

Вместо линейной структуры, где вызываются функции, задается некоторая среда, затем вызываются другие, все на одном уровне, стараются использовать глубокое дерево вызовов с минимальным возвратом данных. Возвращение материала в bash неудобно, если вы принимаете самоналоженное воздержание от глобальных варсов...

Ответ 3

С точки зрения реализации я предлагаю shUnit.

С практической точки зрения, я предлагаю не сдаваться. Я использую TDD для сценариев bash, и я подтверждаю, что это стоит усилий.

Конечно, я получаю в два раза больше строк теста, чем кода, но со сложными сценариями, усилия в тестировании - хорошая инвестиция. Это верно, в частности, когда ваш клиент меняет свое мнение в конце проекта и изменяет некоторые требования. Наличие набора регрессионных тестов - большая помощь в изменении сложного кода bash.

Ответ 4

Если вы закодируете программу bash, достаточно большую для TDD, вы используете неправильный язык.

Я предлагаю вам прочитать мой предыдущий пост о лучших практиках программирования bash, вы, вероятно, найдете что-то полезное, чтобы сделать вашу программу bash проверкой, но мое выражение выше остается.

Шаблоны проектирования или рекомендации для сценариев оболочки

Ответ 5

Написав то, что Meszaros называет потребительские тесты, сложно на любом языке. Другим подходом является проверка поведения команд, таких как rsync вручную, а затем запись единичных тестов для подтверждения конкретных функций без попадания в сеть. В этом слегка модифицированном примере $run используется для печати побочных эффектов, если script выполняется с ключевым словом "test"

function distribute {
    local file=$1 ; shift
    for host in [email protected] ; do
        $run rsync -ae ssh $file $host:$file
    done
}

if [[ $1 == "test" ]]; then
    run="echo"
else
    distribute schedule.txt $*
    exit 0
fi

#    
# Built-in self-tests
#

output=$(mktemp)
expected=$(mktemp)
set -e
trap "rm $got $expected" EXIT

distribute schedule.txt login1 login2 > $output
cat << EOF > $expected
rsync -ae ssh schedule.txt login1:schedule.txt
rsync -ae ssh schedule.txt login2:schedule.txt
EOF
diff $output $expected
echo -n '.'

echo; echo "PASS"

Ответ 6

Вы можете взглянуть на огурец/арубу. У меня была хорошая работа.

Кроме того, вы можете закрыть все, что хотите, выполнив что-то вроде этого:

#
# code.sh
#
some_function_calling_some_external_binary()
{
    if ! external_binary action_1; then
        # ...
    fi  

    if ! external_binary action_2; then
        # ...
    fi
}

#
# test.sh
#

# now for the test, simply stub your external binary:
external_binary()
{
    if [ "[email protected]" = "action_1" ]; then
        # stub action_1

    elif [ "[email protected]" = "action_2" ]; then
        # stub action_2

    else
        external_binary [email protected]
    fi  
}

Ответ 7

В расширенном bash руководстве по сценариям есть пример функции assert, но вот более простая и гибкая функция assert - просто используйте eval $* для проверки любого состояния.

assert() {
  if ! eval $* ; then
      echo
      echo "===== Assertion failed:  \"$*\" ====="
      echo "File \"$0\", line:$LINENO line:${BASH_LINENO[*]}"
      echo line:$(caller 0)
      exit 99
  fi  
}

# e.g. USAGE:
assert [[ $r == 42 ]]
assert "((r==42))"

BASH_LINENO и caller bash builtin являются bash специфичными для оболочки.

Ответ 8

взгляните на Outthentic framework - он предназначен для создания сценариев, которые запускают любой код Bash, а затем анализируют stdout используя формальный DSL, довольно легко создать любой набор тестов Tdd/blackbox на этом инструменте.

Ответ 9

Я не могу поверить, что никто не говорил о OSHT ! Он совместим как с TAP, так и с JUnit, это чистая оболочка (то есть никакие другие языки не задействованы), он тоже работает автономно, и он простой и прямой.

Тестирование выглядит следующим образом (фрагменты взяты со страницы проекта):

#!/bin/bash
. osht.sh

# Optionally, indicate number of tests to safeguard against abnormal exits
PLAN 13

# Comparing stuff
IS $(whoami) != root
var="foobar"
IS "$var" =~ foo
ISNT "$var" == foo

# test(1)-based tests
OK -f /etc/passwd
NOK -w /etc/passwd

# Running stuff
# Check exit code
RUNS true
NRUNS false

# Check stdio/stdout/stderr
RUNS echo -e 'foo\nbar\nbaz'
GREP bar
OGREP bar
NEGREP . # verify empty

# diff output
DIFF <<EOF
foo
bar
baz
EOF

# TODO and SKIP
TODO RUNS false
SKIP test $(uname -s) == Darwin

Простой прогон:

$ bash test.sh
1..13
ok 1 - IS $(whoami) != root
ok 2 - IS "$var" =~ foo
ok 3 - ISNT "$var" == foo
ok 4 - OK -f /etc/passwd
ok 5 - NOK -w /etc/passwd
ok 6 - RUNS true
ok 7 - NRUNS false
ok 8 - RUNS echo -e 'foo\nbar\nbaz'
ok 9 - GREP bar
ok 10 - OGREP bar
ok 11 - NEGREP . # verify empty
ok 12 - DIFF <<EOF
not ok 13 - TODO RUNS false # TODO Test Know to fail

Последний тест показывает как "не в порядке", но код выхода равен 0, потому что это TODO. Можно установить и многословно:

$ OSHT_VERBOSE=1 bash test.sh # Or -v
1..13
# dcsobral \!= root
ok 1 - IS $(whoami) != root
# foobar =\~ foo
ok 2 - IS "$var" =~ foo
# \! foobar == foo
ok 3 - ISNT "$var" == foo
# test -f /etc/passwd
ok 4 - OK -f /etc/passwd
# test \! -w /etc/passwd
ok 5 - NOK -w /etc/passwd
# RUNNING: true
# STATUS: 0
# STDIO <<EOM
# EOM
ok 6 - RUNS true
# RUNNING: false
# STATUS: 1
# STDIO <<EOM
# EOM
ok 7 - NRUNS false
# RUNNING: echo -e foo\\nbar\\nbaz
# STATUS: 0
# STDIO <<EOM
# foo
# bar
# baz
# EOM
ok 8 - RUNS echo -e 'foo\nbar\nbaz'
# grep -q bar
ok 9 - GREP bar
# grep -q bar
ok 10 - OGREP bar
# \! grep -q .
ok 11 - NEGREP . # verify empty
ok 12 - DIFF <<EOF
# RUNNING: false
# STATUS: 1
# STDIO <<EOM
# EOM
not ok 13 - TODO RUNS false # TODO Test Know to fail

Переименуйте его, чтобы использовать расширение .t и поместите его в подкаталог t, и вы можете использовать prove(1) (часть Perl) для его запуска:

$ prove
t/test.t .. ok
All tests successful.
Files=1, Tests=13,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.11 cusr  0.16 csys =  0.31 CPU)
Result: PASS

Установите OSHT_JUNIT или передайте -j чтобы получить вывод JUnit. JUnit также можно комбинировать с prove(1).

Я использовал эту библиотеку как для тестирования функций, получая их файлы, а затем запуская утверждения с IS/OK и их негативами, а также сценарии с помощью RUN/NRUN. Для меня эта структура обеспечивает наибольшую выгоду для наименьших накладных расходов.