Подтвердить что ты не робот

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

Как вы можете проверить частную функцию в 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;
    }

}

Решение, которое я нашел

  • Поместите сам тестовый код внутри замыкания или добавьте код внутри замыкания, в котором хранятся ссылки на локальные переменные на существующие объекты во внешней области.

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

Пожалуйста, предложите мне лучший способ решить эту проблему, если вы сделали что-то?

P.S

  • Большая часть ответа на подобный тип вопроса, подобный этому, не дает решения проблемы, поэтому я задаю этот вопрос

  • Большинство разработчиков говорят, что вы не тестируете частные функции, но я не говорю, что они неправильные или правильные, но для моего случая есть необходимость проверить конфиденциальность.

4b9b3361

Ответ 1

Я с вами, даже если хорошей целью является "только модульное тестирование общедоступного 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. Вы можете использовать доступ к массиву ([]), чтобы получить доступ к закрытым членам:

    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.

Ответ 2

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

Eg.

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.

Ответ 3

Не пишите тесты для частных методов. Это побеждает точку модульных тестов.

  • Вы должны тестировать публичный 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;
  }
}

Не используйте методы и свойства публично, чтобы их проверить. Обычно это означает, что:

  • Вы пытаетесь протестировать реализацию, а не API (открытый интерфейс).
  • Вы должны переместить рассматриваемую логику в свой класс, чтобы упростить тестирование.

Ответ 4

Вы можете вызывать частные методы. Если вы столкнулись со следующей ошибкой:

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(/*...*/);

Ответ 5

Точка "не тестировать частные методы" на самом деле - это тест класса, как тот, кто его использует.

Если у вас есть открытый 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;
    }
}

Unit test защищенные свойства таким же образом, как потребитель использовал бы их через подклассификацию:

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

Ответ 6

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

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

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

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

Мы не смогли сделать это здесь, разрешив объекту FooBar принимать Bar, который не готов в то время, когда FooBar сконструирован, и получил компенсацию путем "взлома" объекта FooBar, чтобы его собственными руками.

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

В вашем примере свойство "статус" Bar не похоже на правильное состояние, в котором FooBar может его принять, поэтому FooBar что-то делает для исправления этой проблемы.

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

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

Ответ 7

Я согласен с @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.

Ответ 8

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


Решение

TL;DR; если метод должен быть протестирован, то вы должны отделить код от класса, чтобы вы могли предоставить метод для публичного тестирования.

Причина, по которой у вас есть метод 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(){
        // ...
    }
}

Ответ 9

Ответ Аарона самый лучший и работает на меня :) Я бы проголосовал, но, к сожалению, не могу (отсутствует репутация).

Я должен сказать, что тестирование частных методов - это единственный способ использовать их и иметь чистый код с другой стороны.

Например:

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), но тестируете любой метод будет.

Ответ 10

Я выбрал этот маршрут, где я создаю функции вне класса и назначаю функцию моему приватному методу.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Теперь я не знаю, какой тип правил ООП я нарушаю, но, чтобы ответить на вопрос, я проверяю частные методы. Я приветствую любого, кто посоветует за и против этого.