IT박스

Jasmine으로 개인 메소드의 Angular / TypeScript에 대한 단위 테스트를 작성하는 방법

itboxs 2020. 6. 17. 19:21
반응형

Jasmine으로 개인 메소드의 Angular / TypeScript에 대한 단위 테스트를 작성하는 방법


각도 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/

이 문제를 해결하는 더 좋은 방법을 제안하십시오.

추신

  1. 이와 비슷한 유형의 질문에 대한 대부분의 대답은 문제에 대한 해결책을 제공하지 않으므로이 질문을하는 이유입니다.

  2. 대부분의 개발자는 개인 기능을 테스트하지 않는다고 말하지만 기능이 잘못되었거나 옳다고 말하지는 않지만 개인의 경우를 테스트해야 할 필요성이 있습니다.


"공용 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 Playground실제 예제입니다 .


대부분의 개발자 는 private function 테스트를 권장하지 않으므로 테스트하지 않겠 습니까?.

예 :

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에게 감사합니다.


개인용 메소드에 대한 테스트를 작성하지 마십시오. 이것은 단위 테스트의 요점을 무너 뜨립니다.

  • 수업의 공개 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. 테스트를 쉽게하려면 문제의 논리를 자체 클래스로 이동해야합니다.

개인 메소드를 호출 할 수 있습니다. 다음 오류가 발생한 경우 :

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

The point of "don't test private methods" really is Test the class like someone who uses it.

If you have a public API with 5 methods, any consumer of your class can use these, and therefore you should test them. A consumer should not access the private methods/properties of your class, meaning you can change private members when the public exposed functionality stays the same.


If you rely on internal extensible functionality, use protected instead of private.
Note that protected is still a public API (!), just used differently.

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 protected properties in the same way a consumer would use them, via subclassing:

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

Sorry for the necro on this post, but I feel compelled to weigh in on a couple of things that do not seem to have been touched on.

First a foremost - when we find ourselves needing access to private members on a class during unit testing, it is generally a big, fat red flag that we've goofed in our strategic or tactical approach and have inadvertently violated the single responsibility principal by pushing behavior where it does not belong. Feeling the need to access methods that are really nothing more than an isolated subroutine of a construction procedure is one of the most common occurrences of this; however, it's kind of like your boss expecting you to show up for work ready-to-go and also having some perverse need to know what morning routine you went through to get you into that state...

The other most common instance of this happening is when you find yourself trying to test the proverbial "god class." It is a special kind of problem in and of itself, but suffers from the same basic issue with needing to know intimate details of a procedure - but that's getting off topic.

In this specific example, we've effectively assigned the responsibility of fully initializing the Bar object to the FooBar class's constructor. In object oriented programming, one of the core tenents is that the constructor is "sacred" and should be guarded against invalid data that would invalidate its' own internal state and leave it primed to fail somewhere else downstream (in what could be a very deep pipeline.)

We've failed to do that here by allowing the FooBar object to accept a Bar that is not ready at the time that the FooBar is constructed, and have compensated by sort-of "hacking" the FooBar object to take matters into its' own hands.

This is the result of a failure to adhere to another tenent of object oriented programming (in the case of Bar,) which is that an object's state should be fully initialized and ready to handle any incoming calls to its' public members immediately after creation. Now, this does not mean immediately after the constructor is called in all instances. When you have an object that has many complex construction scenarios, then it is better to expose setters to its optional members to an object that is implemented in accordance with a creation design-pattern (Factory, Builder, etc...) In any of the latter cases, you would be pushing the initialization of the target object off into another object graph whose sole purpose is directing traffic to get you to a point where you have a valid instance of that which you are requesting - and the product should not be considered "ready" until after this creation object has served it up.

In your example, the Bar's "status" property does not seem to be in a valid state in which a FooBar can accept it - so the FooBar does something to it to correct that issue.

The second issue I am seeing is that it appears that you are trying to test your code rather than practice test-driven development. This is definitely my own opinion at this point in time; but, this type of testing is really an anti-pattern. What you end up doing is falling into the trap of realizing that you have core design problems that prevent your code from being testable after the fact, rather than writing the tests you need and subsequently programming to the tests. Either way you come at the problem, you should still end up with the same number of tests and lines of code had you truly achieved a SOLID implementation. So - why try and reverse engineer your way into testable code when you can just address the matter at the onset of your development efforts?

Had you done that, then you would have realized much earlier on that you were going to have to write some rather icky code in order to test against your design and would have had the opportunity early on to realign your approach by shifting behavior to implementations that are easily testable.


I agree with @toskv: I wouldn't recommend to do that:-)

But if you really want to test your private method, you can be aware that the corresponding code for the TypeScript correspond to a method of the constructor function prototype. This means that it can be used at runtime (whereas you will probably have some compilation errors).

For example:

export class FooBar {
  private _status: number;

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

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

will be transpiled into:

(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);

See this plunkr: https://plnkr.co/edit/calJCF?p=preview.


As many have already stated, as much as you want to test the private methods you shouldn't hack your code or transpiler to make it work for you. Modern day TypeScript will deny most all of the hacks that people have provided so far.


Solution

TLDR; if a method should be tested then you should be decoupling the code into a class that you can expose the method to be public to be tested.

The reason you have the method private is because the functionality doesn't necessarily belong to be exposed by that class, and therefore if the functionality doesn't belong there it should be decoupled into it's own class.

Example

I ran across this article that does a great job of explaining how you should tackle testing private methods. It even covers some of the methods here and how why they're bad implementations.

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

Note: This code is lifted from the blog linked above (I'm duplicating in case the content behind the link changes)

Before
class User{
    public getUserInformationToDisplay(){
        //...
        this.getUserAddress();
        //...
    }

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
After
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}

The answer by Aaron is the best and is working for me :) I would vote it up but sadly I can't (missing reputation).

I've to say testing private methods is the only way to use them and have clean code on the other side.

For example:

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

It' makes a lot of sense to not test all these methods at once because we would need to mock out those private methods, which we can't mock out because we can't access them. This means we need a lot of configuration for a unit test to test this as a whole.

This said the best way to test the method above with all dependencies is an end to end test, because here an integration test is needed, but the E2E test won't help you if you are practicing TDD (Test Driven Development), but testing any method will.


This route I take is one where I create functions outside the class and assign the function to my private method.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Now I don't know what type of OOP rules I am breaking, but to answer the question, this is how I test private methods. I welcome anyone to advise on Pros & Cons of this.

참고URL : https://stackoverflow.com/questions/35987055/how-to-write-unit-testing-for-angular-typescript-for-private-methods-with-jasm

반응형