Код тестування блоку із залежністю від файлової системи


138

Я пишу компонент, який, враховуючи ZIP-файл, повинен:

  1. Розпакуйте файл.
  2. Знайдіть конкретний dll серед розпакованих файлів.
  3. Завантажте цей dll за допомогою відображення та застосуйте метод на ньому.

Я хотів би перевірити цей компонент.

Мені спокуса написати код, який стосується безпосередньо файлової системи:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Але люди часто кажуть: "Не пишіть одиничні тести, які покладаються на файлову систему, базу даних, мережу тощо".

Якби я писав це дружньою темою, я вважаю, що це виглядатиме так:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Так! Тепер це перевіряється; Я можу подавати тестові парні (макети) методу DoIt. Але якою ціною? Тепер мені довелося визначити 3 нові інтерфейси, щоб зробити цей тест. І що саме я тестую? Я перевіряю, чи моя функція DoIt належним чином взаємодіє з її залежностями. Це не перевіряє, чи не було розпаковано zip-файл належним чином тощо.

Не здається, що я вже перевіряю функціональність. Схоже, я просто тестую взаємодію класів.

Моє запитання таке : який правильний спосіб перевірити те, що залежить від файлової системи?

редагувати Я використовую .NET, але концепція також може застосовувати Java або власний код.


8
Люди кажуть, що не записуйте у файлову систему під час тестування одиниць, оскільки якщо ви спокусилися написати у файловій системі, ви не розумієте, що являє собою тест на одиницю. Одиничний тест зазвичай взаємодіє з одним реальним об'єктом (одиниця, що перевіряється), а всі інші залежності висміюються та передаються. Тестовий клас складається з методів тестування, які підтверджують логічні шляхи методами об'єкта та ТОЛЬКО логічними шляхами в блок, що перевіряється.
Крістофер Перрі

1
у вашій ситуації єдиною частиною, яка потребує тестування одиниць, буде те, myDll.InvokeSomeSpecialMethod();коли ви переконаєтесь, що вона працює правильно, як в успішних, так і в невдалих ситуаціях, тому я не став би тестуванням, DoItале DllRunner.Runсказали, що неправильне використання тесту UNIT перевірить, чи працює весь процес прийнятне неправильне використання, і як це було б інтеграційним тестом, що маскує тест одиниці, нормальні правила тестування одиниці не потрібно застосовувати так суворо
MikeT

Відповіді:


47

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

Також було б непогано зайти chdir()у тимчасовий каталог перед запуском тесту та chdir()назад після цього.


27
+1, однак зауважте, що chdir()процес є загальним, щоб ви могли порушити можливість паралельно запускати тести, якщо це підтримує ваш тестовий фреймворк або майбутня версія.

69

Так! Тепер це перевіряється; Я можу подавати тестові парні (макети) методу DoIt. Але якою ціною? Тепер мені довелося визначити 3 нові інтерфейси, щоб зробити цей тест. І що саме я тестую? Я перевіряю, чи моя функція DoIt належним чином взаємодіє з її залежностями. Це не перевіряє, чи не було розпаковано zip-файл належним чином тощо.

Ви вдарили цвяхом прямо по його голові. Те, що ви хочете перевірити, - це логіка вашого методу, не обов'язково, чи можна вирішити справжній файл. Вам не потрібно перевіряти (у цьому тесті на одиницю), чи правильно розпакований файл, ваш метод сприймає це як належне. Інтерфейси цінні самі по собі, оскільки вони надають абстракції, проти яких можна запрограмувати, а не неявно або явно покладатися на одну конкретну реалізацію.


12
Як DoItзазначено, тестувана функція навіть не потребує тестування. Як справедливо зазначав запитуючий, для тестування не залишається нічого важливого. Тепер це реалізація IZipper, IFileSystemі IDllRunnerщо потребує в тестуванні, але вони самі речі , які були знущалися за тест!
Ян Голдбі

56

Ваше запитання розкриває одну з найскладніших частин тестування для розробників, щойно вступають у нього:

"Що, до біса, я тестую?"

Ваш приклад не дуже цікавий, тому що він просто склеює деякі дзвінки API, тож якщо ви писали для нього тест одиниці, ви в кінцевому підсумку просто стверджуєте, що методи були викликані. Такі тести щільно поєднують деталі вашої реалізації з тестом. Це погано, оскільки тепер вам доведеться змінювати тест щоразу, коли ви змінюєте деталі реалізації вашого методу, оскільки зміна деталей про реалізацію порушує ваш тест!

Мати погані тести насправді гірше, ніж взагалі немає тестів.

У вашому прикладі:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Хоча ви можете передавати макети, в методі тестування немає логіки. Якщо ви намагаєтесь перевірити одиничний тест, це може виглядати приблизно так:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Вітаємо, ви в основному скопіювали деталі реалізації свого DoIt()методу в тест. Щасливе обслуговування.

Коли ви пишете тести, ви хочете перевірити, ЩО, а не ЯК . Дивіться тестування чорної скриньки для отримання додаткової інформації.

Що це ім'я вашого методу (або , по крайней мере , має бути). Як все маленькі деталі реалізації , які живуть всередині вашого методу. Хороші тести дозволяють помінятись на ЯК, не порушуючи ЩО .

Подумайте про це так, запитайте себе:

"Якщо я зміню деталі впровадження цього методу (не змінюючи публічний контракт), це порушить мої тести?"

Якщо відповідь "так", ви тестуєте ЯК, а не ЩО .

Щоб відповісти на ваше конкретне питання щодо тестування коду із залежностями файлової системи, скажімо, у вас щось було цікавіше з файлом, і ви хотіли зберегти вміст кодованого Base64 byte[]файлу у файл. Ви можете використовувати потоки для цього, щоб перевірити, що ваш код робить правильно, не перевіряючи, як це робиться. Одним із прикладів може бути щось подібне (на Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Тест використовує, ByteArrayOutputStreamале в додатку (використовуючи ін'єкцію залежностей) справжній StreamFactory (можливо, називається FileStreamFactory) повернеться FileOutputStreamз outputStream()і запише вFile .

Що тут було цікавим, writeце те, що він виписував вміст із закодованого Base64, тому для цього ми перевіряли. Для вашого DoIt()методу це було б більш доцільно перевірити інтеграційним тестом .


1
Я не впевнений, що згоден з вашим повідомленням тут. Ви хочете сказати, що немає необхідності проводити тестування такого методу? Ви в основному говорите, що TDD - це погано? Як якщо б ви зробили TDD, ви не можете написати цей метод, перш ніж написати тест. Або ви покладаєтесь на припущення, що ваш метод не вимагатиме тесту? Причина того, що ВСІ рамки тестування одиниць включають функцію "перевірити", полягає в тому, що її нормально використовувати. "Це погано, тому що тепер вам доведеться змінювати тест кожного разу, коли ви змінюєте деталі реалізації вашого методу" ... ласкаво просимо до світу тестування одиниць.
Ронні

2
Ви повинні перевірити КОНТРАКТ методу, а не його реалізацію. Якщо вам доведеться міняти тест кожного разу, коли ваша зміна цього контракту змінюється, ви переживаєте жахливий час, підтримуючи як кодову програму програми, так і базу тестового коду.
Крістофер Перрі

@ Роні сліпо застосовувати тестування блоку не корисно. Існують проекти різного характеру, і одиничне тестування не є ефективним у всіх. Як приклад, я працюю над проектом, в якому 95% коду стосується побічних ефектів (зауважте, цей важкий побічний характер - це вимога , це суттєва складність, а не випадковість , оскільки він збирає дані з широке розмаїття значущих джерел і представляє це дуже мало маніпуляцій, тому чиста логіка навряд чи є). Тестування одиниць тут не ефективне, інтеграційне тестування є.
Vicky Chijwani

Побічні ефекти слід підштовхувати до країв вашої системи, вони не повинні переплітатись між шарами. По краях ви перевіряєте побічні ефекти, які є поведінкою. Скрізь ви повинні намагатися мати чисті функції без побічних ефектів, які легко перевірити і легко обґрунтувати, повторно використовувати та скласти.
Крістофер Перрі

24

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

Я можу взяти до уваги те, що ваші одиничні тести будуть робити стільки, скільки вони можуть, але це не може бути 100% покриттям. Насправді це може бути лише 10%. Справа в тому, що ваші одиничні тести повинні бути швидкими і не мати зовнішніх залежностей. Вони можуть перевірити випадки на кшталт "цей метод викидає ArgumentNullException, коли ви перейдете в нуль для цього параметра".

Потім я додав би тести інтеграції (також автоматизовані та, ймовірно, використовуючи ту саму рамку тестування одиниць), яка може мати зовнішні залежності та тестові сценарії, такі як ці.

Під час вимірювання покриття кодом я вимірюю тести одиниці та інтеграції.


5
Так, я тебе чую. Там є цей химерний світ, куди ви потрапили, де ви так розв'язалися, що все, що вам залишилося, - це виклики методів на абстрактних об'єктах. Повітряний пух. Коли ви досягнете цієї точки, не здається, що ви справді випробовуєте щось реальне. Ви просто тестуєте взаємодію між класами.
Іуда Габріель Хіманго

6
Ця відповідь помилкова. Тестування блоків не схоже на обмороження, воно більше схоже на цукор. Це запікається в торті. Це частина написання вашого коду ... дизайнерська діяльність. Тому ви ніколи не "забруднюєте" свій код чим-небудь, що "полегшить тестування", оскільки тестування - це те, що полегшує вам написання коду. 99% часу тест важко написати, оскільки розробник написав код перед тестом, і в кінцевому підсумку написав злий нестабільний код
Крістофер Перрі

1
@Christopher: щоб поширити вашу аналогію, я не хочу, щоб мій пиріг нагадував скибочку ванілі, щоб я міг використовувати цукор. Все, що я відстоюю, - це прагматизм.
Кент Бугаарт

1
@Christopher: ваша біографія говорить про це все: "Я ревнивий TDD". Я, з іншого боку, прагматичний. Я роблю TDD там, де вона підходить, а не там, де її немає - ніщо у моїй відповіді не говорить про те, що я не роблю TDD, хоча ви, здається, думаєте, що це робить. І чи це TDD чи ні, я не буду вводити велику кількість складності заради полегшення тестування.
Кент Бугаарт

3
@ChristopherPerry Чи можете ви пояснити, як вирішити початкову проблему ОП тоді TDD-способом? Я постійно натрапляю на це; Мені потрібно написати функцію, єдиною метою якої є виконання дії із зовнішньою залежністю, як у цьому питанні. Тож навіть у сценарії "тестування першого тесту" яким би був цей тест?
Дакс Фол

8

Немає нічого поганого в натисканні на файлову систему, просто вважайте це інтеграційним тестом, а не одиничним тестом. Я б поміняв жорстко закодований шлях на відносний шлях і створив підпапку TestData, щоб містити зіпси для одиничних тестів.

Якщо ваші інтеграційні тести запускають занадто багато часу, тоді розділіть їх, щоб вони не працювали так часто, як ваші швидкі тестові одиниці.

Я погоджуюсь, іноді я думаю, що тестування на основі взаємодії може спричинити занадто багато взаємозв'язку і часто закінчується недостатньою цінністю. Ви дійсно хочете перевірити розпакування файлу тут, а не просто перевірити, чи викликаєте ви правильні методи.


Як часто вони бігають, мало хвилює; ми використовуємо сервер безперервної інтеграції, який автоматично запускає їх для нас. Нас не байдуже, як довго вони триватимуть. Якщо "як довго працювати" не викликає занепокоєнь, чи є причина для того, щоб переходити між тестовими підрозділами та інтеграцією?
Іуда Габріель Хіманго

4
Не зовсім. Але якщо розробники хочуть швидко запустити всі тести на локальному рівні, приємно мати простий спосіб зробити це.
JC.

6

Одним із способів було б написати метод unzip для прийому InputStreams. Тоді модульний тест міг побудувати такий InputStream з масиву байтів, використовуючи ByteArrayInputStream. Вміст цього байтового масиву може бути константою в тестовому коді одиниці.


Гаразд, так що дозволяє вводити потік. Ін'єкційна залежність / МОК. Як щодо частини розпакування потоку у файли, завантаження dll серед цих файлів та виклику методу в цьому dll?
Іуда Габріель Хіманго

3

Здається, це більше тест на інтеграцію, оскільки ви залежаєте від конкретної деталі (файлової системи), яка теоретично може змінитися.

Я б абстрагував код, який стосується ОС, у свій власний модуль (клас, збірка, jar і все, що завгодно). У вашому випадку ви хочете завантажити певну DLL, якщо вона знайдена, тому створіть інтерфейс IDllLoader та клас DllLoader. Чи отримає ваша програма придбати DLL у DllLoader за допомогою інтерфейсу і перевірте, що .. ви не несете відповідальності за розпакування коду.


2

Припускаючи, що "взаємодії файлової системи" добре перевірені в самій рамці, створіть свій метод роботи з потоками та протестуйте це. Відкриття FileStream та передача його методу може бути залишене від ваших тестів, оскільки FileStream.Open добре перевірений творцями рам.


Ви та nsayer по суті однакові пропозиції: змусьте мій код працювати зі потоками. Як щодо частини про розпакування вмісту потоку у файли dll, відкриття цього dll та виклик у ньому функції? Що б ти там робив?
Іуда Габріель Хіманго

3
@JudahHimango. Ці деталі не обов'язково можуть бути перевірені. Ви не можете все перевірити. Абстрагуйте неперевірені компоненти у власні функціональні блоки та припустіть, що вони будуть працювати. Коли ви зіткнетеся з помилкою з тим, як працює цей блок, тоді придумайте тест на нього та вуаляйте. Тестування блоку НЕ означає, що потрібно все перевірити. 100% покриття коду в деяких сценаріях нереально.
Зоран Павлович

1

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


1

Для тестування одиниць я б запропонував включити тестовий файл у свій проект (файл EAR або його еквівалент), а потім використати відносний шлях в одиничних тестах, тобто "../testdata/testfile".

Поки ваш проект правильно експортується / імпортується, ніж повинен працювати ваш тест.


0

Як говорили інші, перше - добре, як інтеграційний тест. Другий тестує лише те, що функція повинна насправді робити, і це все тест, який повинен робити.

Як показано, другий приклад виглядає трохи безглуздо, але він дає вам можливість перевірити, як функція реагує на помилки в будь-якому з кроків. Ви не маєте жодної перевірки помилок у прикладі, але в реальній системі, яка у вас є, і введення залежності дозволить вам перевірити всі відповіді на будь-які помилки. Тоді вартість буде того вартий.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.