Elixir. Пиши тести як бог

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Тести — це те слово, про яке так багато говорять, але так мало роблять. Хотілося б висвітлити цю тему ширше. У цій статті я хотів би розібрати кілька простих ідіом, після яких рівень та якість ваших тестів зросте.

Думаю, що кожен розробник проходив такі стадії:

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]

Розмір тестових даних

Розмір створюваних тестових даних також є важливим пунктом. Я неодноразово зустрічав тести, у яких створювалися 10-50 обʼєктів, щоб перевірити фільтрацію чи роботу певної бізнес-логіки. Такі тести стають справжніми монстрами і час їх виконання може досягати кількох секунд, що не дуже добре вплине на час виконання всіх тестів. І за традицією, розберемо приклад.

# 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 — це функція, яка надає спеціальний статус тому чи іншому об’єкту. Робиться це для того, щоб не дублювати безліч фабрик з 1-2 полями, що відрізняються, а конфігурувати фабрику за замовчуванням на момент створення об’єкта.

# 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.

Висновок

В якості завершення хотілося сказати, що не існує ідеального підходу, але застосування «найкращих практик» інших розробників, приведе вас до тієї «найкращої практики» особисто для вас та вашої команди. Тому не соромтеся експериментувати та виробляти ваш власний підхід та стиль.

👍ПодобаєтьсяСподобалось18
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Якщо чесно, то в принципі саме використання ex_machina (судячи з коду це вона) треба відносити то bad example

Ну перше, то те шо функціонал ex_machina відтворюється самотужки написанням кількох функцій (див. hexdocs.pm/ecto/test-factories.html). Друге, цей підхід це низькорівнева шняга, яка може створювати дуже і дуже багато зайвих записів через оголошені асоціації в фабриках. Це культивується тим, шо API бібліотеки таке, що функція яка оголошує фабрику має арність 0 в базових прикладах, а реалізацію з арністю 1 уникають писати, бо ж то коду більше. Недавно рефакторив такі на проекті, і швидкість тестів вдалося збільшити в 2-3 рази. Далі, щоб створити певний стан системи, треба добре знати схему збереження даних в базі. Це ще працює якось на малих проектах, на великих то повна срака. Особливо коли нові люди працюють над системою. Дуже часто бачив ситуації, коли тест використовує стан, який в принципі не повинен існувати згідно з бізнес-логікою. Або колись він був ок, код, який мав приводити до цього стану, змінився і тепер він неможливий, але він існує в тесті, і тест проходить, бо це може бути неістотно для нього. Або ще, коли дані створюються одні, тест не мав би проходити згідно з встановленими асоціаціями, але він проходить тільки тому, що якісь інші дані було створено під капотом і власне вони задовольняють тест. А, і ще там реалізація sequence галіма, використовується єдиний глобальний Agent.

Підійму трохи тему. Я написав бібліотеку, яка пропонує альтернативний підхід до створення тестових даних (на противагу ex_machina). Ось тут деталі github.com/fuelen/seed_factory

Що я тільки що прочитав? Для чого це? Що таке Elixir? Клікбейтом займаємось?

Теги до статі, такі як #QA проставляє редакція DOU.

Та нє, то так не робиться. Назва статті надана так, що перше що спадає на думку, це якась штука, що повязана з тестуванням. Слово language — це в ви же від себе додали, там могло ще бути framework чи library.

Мені здається єдиний misinformation який виник, це тег QA, на превью в facebook, та в тегах статі. Тому всі хто працює QA, пришли взнати щось нового, а тут якийcь Elixir.

Дякую за фідбек, наступного разу буду уточнювати про що йде мова.
На мій погляд цільова аудиторія це еліксир розробники.

Йєп. Ви праві, тому і я і запитав про клікбейт, в назві статті лише 5 слів і одне з них — тести, що тригерить QA. Але назва дуже промовиста, тому не думаю, що її треба міняти. А от додати пару речень, що ситуація — про написання unit-тестів на мові програмування Elixir, точно б не завадило.

Тю, я навпаки прийшов за тегом «Elixir» і стаття пушка виявилась, за що автору велика дяка!

Підписатись на коментарі