Як написати одиничне тестування для Angular / TypeScript для приватних методів з Jasmine


197

Як ви протестуєте приватну функцію в куті 2?

class FooBar {

    private _status: number;

    constructor( private foo : Bar ) {
        this.initFooBar();

    }

    private initFooBar(){
        this.foo.bar( "data" );
        this._status = this.fooo.foo();
    }

    public get status(){
        return this._status;
    }

}

Я знайшов рішення

  1. Помістіть сам тестовий код всередину закриття або Додати код всередині закриття, який зберігає посилання на локальні змінні існуючих об'єктів у зовнішній області.

    Пізніше викресліть тестовий код за допомогою інструменту. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Підкажіть, будь ласка, кращий спосіб вирішити цю проблему, якщо ви щось зробили?

PS

  1. Більшість відповідей на подібний тип питань, як цей, не дає вирішення проблеми, тому я задаю це питання

  2. Більшість розробників кажуть, що ви не перевіряйте приватні функції, але я не кажу, що вони неправильні чи правильні, але в моєму випадку є необхідність перевірити приватну.


11
тести повинні перевіряти лише публічний інтерфейс, а не приватну реалізацію. Тести, які ви робите на публічному інтерфейсі, повинні охоплювати і приватну частину.
toskv

16
Мені подобається, як половина відповідей насправді повинні бути коментарями. ОП задає питання, як ви X? Прийнята відповідь насправді говорить вам, як робити X. Тоді більшість решти обертаються і кажуть: не тільки я не скажу вам X (що явно можливо), але ви повинні робити Y. Більшість інструментів тестування одиниць (я не тут говоримо лише про JavaScript), здатні перевірити приватні функції / методи. Далі я поясню, чому тому, що, здається, заблукали на землі JS (мабуть, дали половину відповідей).
Кватерніон

13
Доброю практикою програмування є розбиття проблеми на керовані завдання, тому функція "foo (x: type)" буде викликати приватні функції a (x: type), b (x: type), c (y: another_type) і d ( z: yet_another_type). Тепер, тому що foo, керуючи дзвінками та збираючи речі, створює певну турбулентність, як тильні сторони скель у потоці, тіні яких справді важко забезпечити тестування всіх діапазонів. Таким чином, легше переконатися, що кожен підмножина діапазонів є дійсним, якщо ви спробуєте перевірити лише батьківський "foo", тестування діапазону стає дуже складним у випадках.
Кватерніон

18
Це не означає, що ви не тестуєте публічний інтерфейс, очевидно, що ви робите, але тестування приватних методів дозволяє вам протестувати ряд коротких керованих фрагментів (з тієї ж причини, що ви їх написали в першу чергу, чому б ви скасували їх це стосується тестування), і те, що тести на загальнодоступних інтерфейсах є дійсними (можливо, функція виклику обмежує діапазони введення), не означає, що приватні методи не мають помилок, коли ви додаєте більш досконалу логіку та викликаєте їх з інших нові батьківські функції,
Кватерніон,

5
якщо ви перевірили їх належним чином з TDD, ви не будете намагатися розібратися, що, до біса, ви робили пізніше, коли ви повинні були перевірити їх правильно.
Кватерніон

Відповіді:


344

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

У TypeScript я виявив кілька способів отримати доступ до приватних членів заради тестування одиниць. Розглянемо цей клас:

class MyThing {

    private _name:string;
    private _count:number;

    constructor() {
        this.init("Test", 123);
    }

    private init(name:string, count:number){
        this._name = name;
        this._count = count;
    }

    public get name(){ return this._name; }

    public get count(){ return this._count; }

}

Незважаючи на те, TS обмежує доступ до членів класу з використанням private, protected, public, скомпільований JS не має приватних користувачів, так як це не річ в JS. Це суто використовується для компілятора TS. Для цього:

  1. Ви можете стверджувати anyкомпілятор та уникати його попередження про обмеження доступу:

    (thing as any)._name = "Unit Test";
    (thing as any)._count = 123;
    (thing as any).init("Unit Test", 123);
    

    Проблема такого підходу полягає в тому, що компілятор просто не має уявлення про те, що ви робите правильно any, тому ви не отримуєте потрібних помилок типу:

    (thing as any)._name = 123; // wrong, but no error
    (thing as any)._count = "Unit Test"; // wrong, but no error
    (thing as any).init(0, "123"); // wrong, but no error
    

    Це, очевидно, ускладнить рефакторинг.

  2. Ви можете використовувати доступ до масиву ( []) для доступу до приватних членів:

    thing["_name"] = "Unit Test";
    thing["_count"] = 123;
    thing["init"]("Unit Test", 123);
    

    Хоча це виглядає дивно, TSC фактично перевіряє типи, як якщо б ви зверталися до них безпосередньо:

    thing["_name"] = 123; // type error
    thing["_count"] = "Unit Test"; // type error
    thing["init"](0, "123"); // argument error
    

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

Ось робочий приклад на майданчику TypeScript .

Редагування для TypeScript 2.6

Ще одним варіантом, який подобається, є використання // @ts-ignore( додане в TS 2.6 ), яке просто пригнічує всі помилки в наступному рядку:

// @ts-ignore
thing._name = "Unit Test";

Проблема з цим полягає в тому, що він придушує всі помилки в наступному рядку:

// @ts-ignore
thing._name(123).this.should.NOT.beAllowed("but it is") = window / {};

Я особисто вважаю @ts-ignoreкодовий запах, і як кажуть документи:

радимо використовувати ці коментарі дуже економно . [наголос оригінальний]


45
Так приємно почути реалістичну позицію щодо тестування одиниць разом із фактичним рішенням, а не ти стандартною догмою тестера одиниць.
d512

2
Деякі "офіційні" пояснення поведінки (яке навіть наводить тестування одиниць як приклад використання): github.com/microsoft/TypeScript/isissue/19335
Aaron Beall

1
Просто використовуйте `// @ ts-ignore`, як зазначено нижче. сказати лайнеру ігнорувати приватного доступу
Томмазо

1
@Tommaso Так, це ще один варіант, але він має той же недолік використання as any: ви втрачаєте всю перевірку типу.
Аарон Білл

2
Найкраща відповідь, яку я бачив за деякий час, дякую @AaronBeall. А також, дякую tymspy за те, що ви задали оригінальне запитання.
nicolas.leblanc

27

Можна викликати приватні методи . Якщо ви зіткнулися з такою помилкою:

expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
// TS2341: Property 'initFooBar' is private and only accessible within class 'FooBar'

просто використовуйте // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);

це має бути вгорі!
jsnewbie

2
Це, звичайно, ще один варіант. Він страждає від тієї ж проблеми, що і as anyпри тому, що ви втрачаєте будь-яку перевірку типу, насправді ви втрачаєте будь-яку перевірку типу в усьому рядку.
Аарон Білл

20

Оскільки більшість розробників не рекомендують тестувати приватну функцію , чому б не перевірити її ?.

Напр.

YourClass.ts

export class FooBar {
  private _status: number;

  constructor( private foo : Bar ) {
    this.initFooBar({});
  }

  private initFooBar(data){
    this.foo.bar( data );
    this._status = this.foo.foo();
  }
}

TestYourClass.spec.ts

describe("Testing foo bar for status being set", function() {

...

//Variable with type any
let fooBar;

fooBar = new FooBar();

...
//Method 1
//Now this will be visible
fooBar.initFooBar();

//Method 2
//This doesn't require variable with any type
fooBar['initFooBar'](); 
...
}

Завдяки @Aaron, @Thierry Templier.


1
Я думаю, що машинопис видає помилки зв’язку при спробі викликати приватний / захищений метод.
Гудгіп

1
@Gudgip це дасть помилки типу і не компілюється. :)
tymspy

10

Не пишіть тести для приватних методів. Це перемагає точку одиничних тестів.

  • Ви повинні тестувати загальнодоступний API свого класу
  • ВИ НЕ слід перевіряти деталі імліментації вашого класу

Приклад

class SomeClass {

  public addNumber(a: number, b: number) {
      return a + b;
  }
}

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

class SomeClass {

  public addNumber(a: number, b: number) {
      return this.add(a, b);
  }

  private add(a: number, b: number) {
       return a + b;
  }
}

Не оприлюднюйте методи та властивості лише для того, щоб перевірити їх. Зазвичай це означає, що:

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

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

1
Уявіть об'єкт, який дає вам випадкове число між 0 і приватною властивістю x. Якщо ви хочете знати, чи правильно встановлений конструктор x, набагато простіше перевірити значення x, ніж зробити сто тестів, щоб перевірити, чи отримані вами числа знаходяться в потрібному діапазоні.
Гальдор

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

1
І чому не дозволяється тестувати реалізацію? Блок тестів добре виявити несподівані зміни. Коли конструктор чомусь забуде встановити число, тест негайно закінчується і попереджає мене. Коли хтось змінює реалізацію, тест теж не вдається, але я вважаю за краще прийняти один тест, ніж мати невиявлену помилку.
Гальдор

2
+1. Чудова відповідь. @TimJames Розповідати правильну практику або вказати на хибний підхід є самою метою ОГ. Замість того щоб знайти хакі-тендітний спосіб досягти всього, чого хоче ОП.
Syed Aqeel Ashiq

4

Сенс "не тестувати приватні методи" насправді полягає в тестуванні класу, як той, хто ним користується .

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


Якщо ви покладаєтесь на внутрішню розширювану функціональність, використовуйте protectedзамість цього private.
Зауважте, що protectedвсе ще є загальнодоступним API (!) , Просто використовується по-іншому.

class OverlyComplicatedCalculator {
    public add(...numbers: number[]): number {
        return this.calculate((a, b) => a + b, numbers);
    }
    // can't be used or tested via ".calculate()", but it is still part of your public API!
    protected calculate(operation, operands) {
        let result = operands[0];
        for (let i = 1; i < operands.length; operands++) {
            result = operation(result, operands[i]);
        }
        return result;
    }
}

Аналіз захищених властивостей блоку таким же чином, як споживач використовував би їх за допомогою підкласи:

it('should be extensible via calculate()', () => {
    class TestCalculator extends OverlyComplicatedCalculator {
        public testWithArrays(array: any[]): any[] {
            const concat = (a, b) => [].concat(a, b);
            // tests the protected method
            return this.calculate(concat, array);
        }
    }
    let testCalc = new TestCalculator();
    let result = testCalc.testWithArrays([1, 'two', 3]);
    expect(result).toEqual([1, 'two', 3]);
});


2

Вибачте за некро в цій публікації, але я змушений зважитися на пару речей, які, здається, не торкалися.

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

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

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

Нам тут цього не вдалося, дозволивши об’єкту FooBar прийняти смугу, яка не готова до того часу, коли FooBar будується, і компенсували своєрідним "злому" об'єкта FooBar, щоб взяти питання у свої власні. руки.

Це є наслідком відмови від дотримання іншого об'єктно-орієнтованого програмування (у випадку з Bar), що полягає в тому, що стан об'єкта повинен бути повністю ініціалізований і готовий обробляти будь-які вхідні дзвінки до його публічних членів одразу після створення. Тепер це не означає одразу після виклику конструктора у всіх випадках. Якщо у вас є об'єкт, який має багато складних сценаріїв побудови, тоді краще виставити сетерів для його необов'язкових членів об’єкту, який реалізується відповідно до схеми створення дизайну (Factory, Builder тощо). останні випадки,

У вашому прикладі властивість "статусу" Bar не здається в дійсному стані, в якому FooBar може його прийняти - тому FooBar робить щось для того, щоб виправити цю проблему.

Друге питання, яке я бачу, це те, що, здається, ви намагаєтеся перевірити свій код, а не практикувати тестові розробки. Це, безумовно, моя власна думка на даний момент часу; але цей тип тестування дійсно є антидіаграмою. Що ви в кінцевому підсумку робите, потрапляєте в пастку розуміння того, що у вас є основні проблеми дизайну, які не дозволяють вашому коду перевірятись після цього факту, а не писати потрібні вам тести та згодом програмувати на тести. У будь-якому випадку, якщо ви зіткнетеся з проблемою, вам слід все-таки закінчити однакову кількість тестів і рядків коду, якби ви справді досягли реалізації SOLID. Отже - навіщо намагатися інженеру повернути свій шлях до перевіряемого коду, коли ви можете просто вирішити цю проблему на початку ваших зусиль з розвитку?

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


2

Я згоден з @toskv: Я б не рекомендував цього робити :-)

Але якщо ви дійсно хочете перевірити свій приватний метод, ви можете знати, що відповідний код для TypeScript відповідає методу прототипу функції конструктора. Це означає, що його можна використовувати під час виконання (тоді як ви, мабуть, матимете деякі помилки компіляції).

Наприклад:

export class FooBar {
  private _status: number;

  constructor( private foo : Bar ) {
    this.initFooBar({});
  }

  private initFooBar(data){
    this.foo.bar( data );
    this._status = this.foo.foo();
  }
}

буде перекладено на:

(function(System) {(function(__moduleName){System.register([], function(exports_1, context_1) {
  "use strict";
  var __moduleName = context_1 && context_1.id;
  var FooBar;
  return {
    setters:[],
    execute: function() {
      FooBar = (function () {
        function FooBar(foo) {
          this.foo = foo;
          this.initFooBar({});
        }
        FooBar.prototype.initFooBar = function (data) {
          this.foo.bar(data);
          this._status = this.foo.foo();
        };
        return FooBar;
      }());
      exports_1("FooBar", FooBar);
    }
  }
})(System);

Дивіться цей плагін: https://plnkr.co/edit/calJCF?p=preview .


1

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


Рішення

TLDR ; якщо метод повинен бути перевірений, то вам слід роз'єднати код у клас, який ви можете викрити методом, який буде відкритим для тестування.

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

Приклад

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

https://patrickdesjardins.com/blog/how-to-unit-test-private-method-in-typescript-part-2

Примітка . Цей код знято з блогу, пов’язаного вище (я копіюю, якщо зміст, що знаходиться за посиланням, зміниться)

До цього
class User{
    public getUserInformationToDisplay(){
        //...
        this.getUserAddress();
        //...
    }

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
Після
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}

1

викликати приватний метод за допомогою квадратних дужок

Файл Ts

class Calculate{
  private total;
  private add(a: number) {
      return a + total;
  }
}

файл spekt.ts

it('should return 5 if input 3 and 2', () => {
    component['total'] = 2;
    let result = component['add'](3);
    expect(result).toEqual(5);
});

0

Відповідь Аарона - найкраща і працює на мене :) Я би проголосував за неї, але, на жаль, не можу (відсутня репутація).

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

Наприклад:

class Something {
  save(){
    const data = this.getAllUserData()
    if (this.validate(data))
      this.sendRequest(data)
  }
  private getAllUserData () {...}
  private validate(data) {...}
  private sendRequest(data) {...}
}

"Немає сенсу перевіряти всі ці методи одразу, тому що нам потрібно знущатись із тих приватних методів, з яких ми не можемо знущатися, бо не можемо отримати до них доступ. Це означає, що нам потрібно багато конфігурації для тестування одиниці, щоб перевірити це в цілому.

Це говорить про те, що найкращий спосіб протестування вищевказаного методу з усіма залежностями - це тест для завершення, оскільки тут потрібен тест на інтеграцію, але тест E2E не допоможе вам, якщо ви практикуєте TDD (Test Driven Development), але тестуйте будь-який метод буде.


0

Цей маршрут, який я беру, - це той, де я створюю функції поза класом і призначаю функцію моєму приватному методу.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

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

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