LINUX.ORG.RU

Unit-тестирование bash скриптов

 


0

1

Всем привет! Кто-нибудь когда-нибудь проводил нечто подобное? Если да, поделитесь опытом. Говорю сразу, я от тестирования очень далёк, знаю, что существует shunit, но мне нужно написать пару тестов именно вручную. Как это сделать? С чего начать? Нужно ли бить скрипт на несколько файлов (юнитов) или воротить всё прямо в нём? Сам скрипт очень простой, интерес чисто академический. Суть: принимает на вход некоторые параметры и в зависимости от них выводит хэловорлд разными способами. Наткнулся в интернете на такую конструкцию:

#!/bin/bash
 set -e
 errors=0
 results=$($script_under_test $args<<ENDTSTDATA
 # inputs
 # go
 # here
 #
 ENDTSTDATA
 )
 [ "$?" -ne 0 ] || {
     echo "Test returned error code $?" 2>&1
     let errors+=1
     }

 echo "$results" | grep -q $expected1 || {
      echo "Test Failed.  Expected $expected1"
      let errors+=1
 }
 # and so on, et cetera, ad infinitum, ad nauseum
 [ "$errors" -gt 0 ] && {
      echo "There were $errors errors found"
      exit 1
 }
Что тут куда совать? Не в техразделе, потому что это просто такая игра. При чём тут linux скажу по секрету, потом, если захотите...

Перемещено mono из talks



Последнее исправление: mono (всего исправлений: 1)

При чём тут linux скажу по секрету

Говори, нам всем очень интересно.

drull
()

Напиши на баше скрипт, который будет вызывать тестируемый скрипт, пропускать ответ через grep или ещё какую дрянь и анализировать ответ.
Или что ты хотел спросить?

Stahl
()

Не тестировать

Писать на bash'е что-то более 100 строк есть мучение и риск, а меньше 100 строк можно и без модульного тестирования отладить.

Camel ☕☕☕
()
Ответ на: комментарий от Stahl

Тестирование всех аспектов

Напиши на баше скрипт, который будет вызывать тестируемый скрипт, пропускать ответ через grep или ещё какую дрянь и анализировать ответ.

А если сценарий не только выводит что-то в выхлоп, но ещё вызывает команды (mdadm, vgchange, mkfs...), пишет в файлы? Чтобы проверять вызов этих команд нужна поддержка каких-то заменителей, mock-объектов. Как это делать в bash'е? Мученье.

Camel ☕☕☕
()
Ответ на: комментарий от Stahl

Это же юнит-тестирование, а оно предполагает тестирование именно отдельных частей, а не всего скрипта разом.

Pyzia
() автор топика
Последнее исправление: Pyzia (всего исправлений: 1)
Ответ на: комментарий от Pyzia

Похоронить

Есть скрипт на баше в 500 строк, предлагаете переписать его на...чём? Питон? Перл? Паскаль? Groovy?

Беда. Переписать я предлагаю на Ruby, но ещё нужно оценить затраты на переписывание.

Camel ☕☕☕
()
Ответ на: комментарий от Stahl

Yo dawgНапиши на баше скрипт, который будет вызывать тестируемый скрипт, пропускать ответ через скрипт и анализировать ответ скриптом.

heilkitty
()
Ответ на: комментарий от heilkitty

Ты забыл в конце написать «скрипт». А то ни разу не грека через реку.

Stahl
()
Последнее исправление: Stahl (всего исправлений: 1)
Ответ на: комментарий от Pyzia

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

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

Hjorn
()
Ответ на: комментарий от Hjorn

Неее, чёто тут не так. Он в процессе выполнения присваивает значения разным переменным, потом в зависимости от значения этих переменных выполняет разные действия, в т.ч. присваивает значения другим переменным и так далее, т.е. этот хэловорд не так прост, нужно тестировать именно кусками.

Pyzia
() автор топика
Ответ на: комментарий от Pyzia

Вот я ж и говорю, трахаться придется с сайд-эффектами. Оно тебе надо?

Hjorn
()

разделяй и властвуй

anonymous
()

Не вижу смысла в unit-тестах для bash-скриптов, так как в большинстве случаев отладка происходит в процессе написания скрипта. Единственное что стоит проверять - это зависимости скрипта от других программ/скриптов. Например, вот тут есть хороший пример функции require - http://linuxhub.ru/viewtopic.php?f=23&t=1911

Punk
()
Ответ на: комментарий от Pyzia

Он в процессе выполнения присваивает значения разным переменным, потом в зависимости от значения этих переменных выполняет разные действия, в т.ч. присваивает значения другим переменным

дай угадаю: ты не слышал про слово local в bash? Тогда ты ССЗБ и тебе никакое тестирование не поможет.

Глобальные переменные работают лишь если строк меньше 50. Если больше, то это Ад и Израиль.

Т.ч. используй локальные переменные внутри bash-функций, и тогда всё будет работать почти без отладки.

emulek
()
Ответ на: комментарий от ritsufag

Рекомендую писать на нормальном яп, а не клее для гнутых утилит.

в умелых руках и пинус — молоток.

Для утилиток в 500 строк сложно найти что-то лучше bash'а.

emulek
()
Ответ на: комментарий от emulek

дай угадаю: ты не слышал про слово local в bash?

От local в баше проку — ноль. Ф-ции возвращают только код исполнения, все остальное приходится писать в какие-нибудь переменные и не ограничивать их областью видимости ф-ции. Это Ад и Израиль, но другого баша у нас нет :(

iVS
()
Последнее исправление: iVS (всего исправлений: 2)

Проще переписать на какой-нибудь python, а там unittest уже из коробки есть.

Kilte
()
Ответ на: комментарий от iVS

Ф-ции возвращают только код исполнения, все остальное приходится писать в какие-нибудь переменные и не ограничивать их областью видимости ф-ции. Это Ад и Израиль, но другого баша у нас нет :(

это твоя проблема.

emulek
()
Ответ на: комментарий от emulek

Не только моя, раз во всяких руби и питонах ф-ции возвращают значения. В баше global state by design, а local — костыль.

iVS
()
Ответ на: комментарий от iVS

для программ ≤500 строк нет нужды возвращать изолированные переменные.

ты можешь использовать локальные переменные для передачи значений между функциями:

#!/bin/bash

X=123;

function foo()
{
	echo "foo() ::X=$X"
	local X=456
	echo "foo() foo::X=$X"
	bar 789
	echo "foo() foo::X=$X"
}

function bar()
{
	echo "bar() foo::X=$X"
	echo "bar() increment foo::X"
	(( X++ ))
	echo "bar() foo::X=$X"
	local X="$1"
	echo "bar() bar::X=$X"
	echo "bar() decrement bar::X"
	(( X-- ))
	echo "bar() bar::X=$X"
}


echo "::X=$X"
foo
echo "::X=$X"

тут три переменных X. функция bar() может вернуть значение через foo::X, которая не является глобальной.

emulek
()
Ответ на: комментарий от emulek

для программ ≤500 строк нет нужды возвращать изолированные переменные.

ты можешь использовать локальные переменные для передачи значений между функциями:

Собственно, все, что ты делаешь — передаешь один раз значение bar 789. Возможно, это дело вкуса, но я предпочту давать переменным осмысленные названия, особенно, когда это не просто значение, а результат предыдущих нетривиальных действий.

iVS
()
Ответ на: комментарий от iVS

лови PoC

#!/bin/bash

function factorial_r()
{
	if (( x<=1 )); then
		y=$x
		return
	fi
	(( y = x - 1 ))
	local x=$y
	factorial_r
	(( y *= x ))
	# echo "$y"
}

function factorial()
{
	local y
	local x=$1
	factorial_r
	(( y *= x ))
	echo "$x! = $y"
}

factorial 7

что тут сложного?

emulek
()
Ответ на: комментарий от iVS

Возможно, это дело вкуса, но я предпочту давать переменным осмысленные названия

я тоже так делаю. А в чём проблема?

emulek
()
Ответ на: комментарий от emulek

Конечно, писать одну ф-цию и говорить «что тут сложного» каждый может. Но возьми одну ф-цию, вызови из нее другую и считай результат в первую с помощью локальных переменных. Вот простой пример:

foo() {
  X=1
  bar
  echo "$X"
}
bar() {
  (( X+=1 ))
}
foo
# 2

iVS
()
Ответ на: комментарий от iVS

# 2

а ты сколько хотел? У тебя X глобальное, и получается 1+1=2. Сделай X локальным внутри bar, если хочешь, что-бы внутри foo не менялось. Причём можно X внутри foo тоже сделать локальным, что-бы не было сайд-эффекта с глобальным X.

Конечно bash это тебе не C++, тут namespace'ов нет, да и если ты сделал локальное X, то до другого X(перекрытого) уже не добраться. Т.е. не получится писать myspace::X, myclass::X, и даже ::X. Да и ладно, это не нужно в программах на 500 строк.

Главное, что есть полноценные изолированные контексты для функций, т.е. сайд-эффекты можно исключить. А если ты не осилил — ССЗБ.

emulek
()
Ответ на: комментарий от emulek

Ты забыл препарат принять! Как отпустит, почитай и попробуй понять, что я писал:

Но возьми одну ф-цию, вызови из нее другую и считай результат в первую с помощью локальных переменных.

iVS
()
Ответ на: комментарий от iVS

не тупи. Здесь Unit-тестирование bash скриптов (комментарий) я сделал именно то, что ты просишь:

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

причём сделал я это рекурсивно, что-бы ты не сомневался, что переменные в баше по настоящему локальны.

У меня исходные данные передаются в x, а результат возвращается в y. Т.к. результат для рекурсии не нужен, то на всех итерациях одна локальная y. А вот x для каждой итерации своя. Для лучшего понимания вставь второй строкой

set -x

emulek
()
Ответ на: комментарий от emulek

Здесь Unit-тестирование bash скриптов (комментарий) я сделал именно то, что ты просишь

Переменная y у тебя объявляется только в функции factorial и доступна из всех вызываемых рекурсивно ф-ций. Т.е., вместо действительно рекурсии f(n)=n*f(n-1) у тебя глобальная переменная y, которую ты изменяешь в каждой из ф-ций. А ты подменяешь мне понятия, потому что область видимости по-настоящему локальной переменной — только внутри одной ф-ции, где переменная объявляется. В этом смысле у тебя y не локальна и служит костылем для передачи значения из одной ф-ции в другую.

iVS
()
Ответ на: комментарий от iVS

Переменная y у тебя объявляется только в функции factorial и доступна из всех вызываемых рекурсивно ф-ций. Т.е., вместо действительно рекурсии f(n)=n*f(n-1) у тебя глобальная переменная y, которую ты изменяешь в каждой из ф-ций

это не баг, а фича. y локальная в factorial, но действительно видимая из любой factorial_r. А вот переменная x как ты хочешь: в любой итерации своя.

А ты подменяешь мне понятия, потому что область видимости по-настоящему локальной переменной — только внутри одной ф-ции, где переменная объявляется.

это в твоём любимом язычке так? Ну не везде это так. Например в сишке вполне можно получать доступ к переменным вызывающей функции(но не наоборот). А у тебя очевидно какие-то ненужные костыли. И наверняка есть костыли для обхода этого костыля. «Замыкания» какие-нить, да?

И всё это лирика, на практике сайдэффектов не будет с локальными переменными в bash'е. Чем ты недоволен?

emulek
()
Ответ на: комментарий от Pyzia

Есть скрипт на баше в 500 строк, предлагаете переписать его на...чём? Питон? Перл? Паскаль? Groovy?

Да хоть на JavaScript. Любой нормальный скриптовой язык лучше чем Bash.

rupert 😊😊😊😊😊
()
Ответ на: комментарий от Pyzia

bash я худо-бедно знаю, а вот Ruby ещё нужно учить.

За пару дней худо-бедно подучишь Ruby и начнёшь на нём писать. А потом в процессе использования выучишь его ещё лучше.

rupert 😊😊😊😊😊
()
Ответ на: комментарий от Pyzia

Беда в том, что bash я худо-бедно знаю, а вот Ruby ещё нужно учить

пиши на python

anonymous
()
Ответ на: комментарий от emulek

И всё это лирика, на практике сайдэффектов не будет с локальными переменными в bash'е. Чем ты недоволен?

Да не, я просто убедился, что моё понимание подхода в bash'е верно: заводишь переменную, которая видна из вызываемой ф-ции тоже, и туда складываешь результат. Хоть я считаю, что возвращать значение из ф-ции было бы логичнее, что соотв. не «моему любимому язычку», а следует из мат. определения ф-ции. В bash было бы логичнее называть их не ф-циями, а процедурами, имхо. Но заморачиваться этим вопросом не принято для скриптов не более 500 строк. Спасибо за разъяснения.

iVS
()
Ответ на: комментарий от iVS

Да не, я просто убедился, что моё понимание подхода в bash'е верно: заводишь переменную, которая видна из вызываемой ф-ции тоже, и туда складываешь результат.

да, но не глобальную.

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

да. Как и в системе, «функции» в bash возвращают код возврата, фактически bool. Т.е. OK/0 если всё хорошо, и FAIL/неноль если ошибка. Кроме как выделить несколько вариантов ошибок(e.g. EINVAL, ENOENT, EACCESS, etc) эти коды не для чего не нужны.

А выходные параметры надо передавать как-то по другому, через переменные/файлы.

emulek
()

Пришёл к такой конструкции:

func ()
{
if [ "$y" != "$x" ]
 then
 let "error_count += 1"
fi
}

var1=$y
var2="x"
func
#Здесь сравниваются значения переменных, которые получены после прохождения скрипта с значением, которое должно быть, но таких значений много и приходится много раз делать
var1=$y
var2="x"
func
И каждый раз с новыми значениями. Как бы мне это всё зациклить, чтобы избавиться от повторяющегося кода? Да, я дерево...

Pyzia
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.