Как написать модульное тестирование для Angular / TypeScript для частных методов с Jasmine


199

Как вы тестируете приватную функцию в angular 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 (что вполне возможно), но вы должны делать Y. Большинство инструментов модульного тестирования (я не говорить только о JavaScript здесь) способны тестировать частные функции / методы. Я продолжу объяснять, почему, потому что он, кажется, затерялся на земле JS (очевидно, учитывая половину ответов).
Quaternion

13
Хорошая практика программирования - разбивать проблему на управляемые задачи, поэтому функция "foo (x: type)" будет вызывать закрытые функции a (x: type), b (x: type), c (y: another_type) и d ( г: yet_another_type). Теперь, поскольку foo управляет вызовами и собирает вещи вместе, это создает своего рода турбулентность, как обратные стороны камней в потоке, тени, которые действительно трудно гарантировать, что все диапазоны проверены. Таким образом, легче гарантировать, что каждый поднабор диапазонов является действительным, если вы попытаетесь протестировать только родительский "foo", то тестирование диапазонов становится очень сложным в случаях.
Quaternion

18
Это не значит, что вы не тестируете публичный интерфейс, очевидно, вы это делаете, но тестирование приватных методов позволяет вам тестировать серию коротких управляемых кусков (по той же причине, по которой вы написали их в первую очередь, почему вы должны отменить это когда речь идет о тестировании), и то, что тесты на общедоступных интерфейсах действительны (возможно, вызывающая функция ограничивает диапазоны ввода), не означает, что закрытые методы не имеют недостатков, когда вы добавляете более продвинутую логику и вызываете их из других новые родительские функции,
Quaternion

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

Ответы:


349

Я с вами, хотя хорошей целью является «только модульное тестирование общедоступного 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. Therefor:

  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. Вы можете использовать массив access ( []), чтобы получить доступ к закрытым членам:

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

Редактировать для 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кодовый запах и, как говорят доктора:

мы рекомендуем вам использовать эти комментарии очень экономно . [Акцент оригинал]


46
Так приятно слышать реалистичную позицию по модульному тестированию вместе с реальным решением, а не стандартную догму тестера модулей.
d512

2
Некоторое «официальное» объяснение поведения (которое даже цитирует модульное тестирование как пример использования): github.com/microsoft/TypeScript/issues/19335
Аарон

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

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
Я думаю, что машинопись выдает ошибки linting, когда вы пытаетесь вызвать закрытый / защищенный метод.
Gudgip

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 Рассказывать правильную практику или указывать на некорректный подход - это и есть цель SO. Вместо того, чтобы найти хрупко хрупкий способ добиться того, чего хочет ОП.
Сайед Акил Ашик

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]);
});

3

Это сработало для меня:

Вместо того:

sut.myPrivateMethod();

Это:

sut['myPrivateMethod']();

2

Извините за некро в этом посте, но я чувствую себя обязанным взвесить пару вещей, которые, кажется, не были затронуты.

Прежде всего - когда во время модульного тестирования нам нужен доступ к закрытым членам класса, это, как правило, большой, толстый красный флаг, который мы обманываем в нашем стратегическом или тактическом подходе и непреднамеренно нарушаем принцип единственной ответственности, подталкивая поведение там, где оно не принадлежит. Чувство необходимости доступа к методам, которые на самом деле являются не чем иным, как изолированной подпрограммой процедуры построения, является одним из наиболее распространенных случаев этого; тем не менее, это похоже на то, что ваш начальник ожидает, что вы явитесь на работу, готовый к работе, а также имеет некоторую извращенную потребность знать, через какую утреннюю рутину вы прошли, чтобы привести вас в это состояние ...

Другой наиболее распространенный случай этого происходит, когда вы пытаетесь проверить общеизвестный «класс бога». Это особая проблема сама по себе, но она страдает от той же самой основной проблемы, связанной с необходимостью знать интимные детали процедуры - но это выходит за рамки темы.

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

Мы не смогли сделать это здесь, позволив объекту FooBar принять Bar, который не был готов в момент создания FooBar, и компенсировали своего рода «взломом» объекта FooBar, чтобы все стало по-своему. Руки.

Это является результатом отказа придерживаться другого принципа объектно-ориентированного программирования (в случае Bar), который заключается в том, что состояние объекта должно быть полностью инициализировано и готово обрабатывать любые входящие вызовы его «открытых членов» сразу после создания. Теперь это не означает, что сразу после вызова конструктора во всех случаях. Когда у вас есть объект, который имеет много сложных сценариев построения, тогда лучше выставить сеттеры для его необязательных членов для объекта, который реализован в соответствии с шаблоном создания проекта (Factory, Builder и т. Д.) В любом из последние случаи,

В вашем примере свойство Bar «status», по-видимому, не находится в допустимом состоянии, в котором 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 ; если метод должен быть протестирован, то вы должны отделить код от класса, чтобы вы могли предоставить метод для публичного тестирования.

Причина, по которой у вас есть метод private, заключается в том, что функциональность не обязательно должна быть доступна для этого класса, и поэтому, если функциональность там не принадлежит, ее следует разделить на собственный класс.

пример

Я наткнулся на эту статью, которая объясняет, как вам следует тестировать частные методы. Здесь даже рассматриваются некоторые из методов и почему они являются плохими реализациями.

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;
  }
}

файл spect.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.