Elixir. Пиши тести як бог
Тести — це те слово, про яке так багато говорять, але так мало роблять. Хотілося б висвітлити цю тему ширше. У цій статті я хотів би розібрати кілька простих ідіом, після яких рівень та якість ваших тестів зросте.
Думаю, що кожен розробник проходив такі стадії:
1. Заперечення.
«Та навіщо мені взагалі потрібні ваші тести». Справді, бувають такі ситуації, коли простіше понатискати кнопки та перевірити функціонал «в бою». Можливо, деякі кейси будуть втрачені, але завдання буде виконано.
2. Розпач
«Ну не виходить у мене писати тести, та й без них нормально». Така думка притаманна розробникам, які трохи застрягли і не можуть зрушити з мертвої точки.
3. Прийняття
«Ах, ось воно що». У цей момент ми дійсно розуміємо, що без тестів нікуди, і необхідно вкладати сили в те, щоб тести були якісно написані та легко підтримувані.
У результаті, ми підходимо до того питання, як домогтися «якості». Якість не має єдиного підходу, згідно з яким у вас вийде все ідеально і можна буде показати сусідові і сказати: «Дивись як у мене вийшло». Але ми можемо виділити основні кроки, звернувши увагу на які, вам вдасться досягти успіху в такій справі як тестування коду.
Розглянемо далі наступні кроки, деякі з них стилістичного характеру.
describe та test
Однією з фундаментальних речей є зрозуміти і коротко пояснити, що відбувається в конкретному тесті.
describe макрос описує випадок, функцію, дію і об’єднує тести за однаковою ознакою. У 99% випадків опис зводиться до того, яку функцію ми тестуємо та її арність.
# bad examples describe "hello" do ... end describe "My new function hello/2" do ... end # good examples describe "hello/2" do ... end describe "GET /users/:id" do ... end describe "when user is active" do ... end
Секція з поганими прикладами демонструє опис, який або не до кінця розкриває предмет тестування, або навантажує його непотрібними деталями. У той же час секція з хорошими прикладами дає чітко зрозуміти, що тестується, і не вводить в оману розробника.
Останній приклад — це додаткова опція того, як можна використовувати describe. У наведеному випадку він вказує на конкретний контекст, це буде корисно у тестуванні модулів з однією точкою входу (публічною функцією).
test макрос описує безпосередньо предмет тестування. Саме в цьому випадку починаються складності та проявляється потенціал «письменника».
# bad examples describe "create_user/2" do test "should have a success result" do ... end test "with invalid name" do ... end end # good examples describe "create_user/2" do test "returns {:ok, %User{}} with valid params" do ... end test "returns {:error, %Ecto.Changeset{}} when user name is invalid" do ... end end describe "GET /users/:id" do test "returns user, 200 status with valid params" do ... end test "returns error, 404 status when user doesn't exist" do ... end end # describe as a context describe "when user is active" do test "returns {:ok, %User{}}" do ... end end describe "when user is inactive" do test "returns {:error, :user_not_found}" do ... end end
Думаю, ви вже помітили, що кожен опис починається з returns... і в цьому слові приховано все рішення. Ми завжди щось повертаємо, чи це буде атом :ok, {:error, _} або щось екзотичніше {:ok, [%User{}, ...]}. Такий опис допоможе програмісту швидше зорієнтуватися і без вникання у нутрощі зрозуміти, що ж тут відбувається.
setup_all и setup
Цей функціонал дозволяє організувати різні дії перед тим, як запустити самі тести. Це може бути створення необхідних даних, запуск процесів або робота з внутрішньою структурою папок та файлів.
setup [:clean_up_tmp_directory] def clean_up_tmp_directory(_context) do ... :ok end setup_all do [conn: Plug.Conn.build_conn()] end setup_all do MemoryCache.start_link end
Важливим моментом буде підкреслити те, що не варто йти в крайнощі і намагатися все запхати в setup. Також можна організувати красивий setup за допомогою приватних функцій, причому ми можемо заміксувати їх, наприклад setup [:base_setup, :user_setup]
Розмір тестових даних
Розмір створюваних тестових даних також є важливим пунктом. Я неодноразово зустрічав тести, у яких створювалися
# bad example describe "list_users/1" do setup do application = insert(:application) _users = insert_list(10, :user, application: application) _homeless_users = insert_list(5, :user) [application_id: application.id] end test "returns users filtered by application_id", %{application_id: application_id} do users = Users.list_users(application_id) assert length(users) == 10 end end # good example describe "list_users/1" do setup do application = insert(:application) user = insert(:user, application: application) _homeless_user = insert(:user) [application_id: application.id, user_id: user.id] end test "returns users filtered by application_id", %{application_id: application_id, user_id: user_id} do assert [%User{id: ^user_id}] = Users.list_users(application_id) end end
Приклад досить простий, але явно демонструє, що достатньо пари користувачів з різними звʼязками, щоб перевірити правильність роботи фільтра.
Factories
Фабрики — допоміжні модулі, які дозволяють наповнювати базу тестовими даними без будь-яких проблем.
Прозоре створення об’єктів
У фабриці можна описувати всі залежності, які необхідно створити. Робиться це для того, щоб задовольнити вимоги щодо зовнішніх ключів, або щоб об’єкт, який ви створюєте, був більш заповненим. Але такі дії мають свої наслідки, ви не контролюєте процес створення запису в БД. Погляньмо на приклад.
# bad example application = build(:application) |> with_users() |> insert() user = hd(application.users) # good example application = insert(:application) user = insert(:user, application: application)
У першому випадку ми створюємо програму разом із користувачами всередині. Як результат, ми не контролюємо кількість створених записів і динамічно вибираємо користувача із ланцюга. З цього можуть випливати моменти, коли наші тести стануть плаваючими.
Другий випадок гарантує нам прозорість створення користувача без прихованого створення зв’язків усередині. Тим самим гарантує стійкість до плаваючих тестів.
Traits як опис спеціальних випадків
Trait — це функція, яка надає спеціальний статус тому чи іншому об’єкту. Робиться це для того, щоб не дублювати безліч фабрик з
# bad example def admin_factory do %User{ name: sequence(:name, &"User #{&1}"), email: sequence(:email, &"user_#{&1}@gmail.com"), role: :admin } end def manager_factory do %User{ name: sequence(:name, &"User #{&1}"), email: sequence(:email, &"user_#{&1}@gmail.com"), role: :manager } end # good example def user_factory do %User{ name: sequence(:name, &"User #{&1}"), email: sequence(:email, &"user_#{&1}@gmail.com") } end def admin(user) do %{user | role: :admin} end def manager(user) do %{user | role: :manager} end # usage build(:user) |> admin() |> insert()
З прикладу ми бачимо, що ми розширюємо поведінку створення користувача і не навантажуємо наші фабрики надмірними кейсами, тим самим слідуємо підходу DRY.
Висновок
В якості завершення хотілося сказати, що не існує ідеального підходу, але застосування «найкращих практик» інших розробників, приведе вас до тієї «найкращої практики» особисто для вас та вашої команди. Тому не соромтеся експериментувати та виробляти ваш власний підхід та стиль.
14 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів