좋은 단원 테스트 작성하기;데이터베이스 연결을 에뮬레이트하지 마십시오.


이 게시물Writing Good Unit Tests; Don’t Mock Database Connections은 처음Qvault에 실렸다.
개발자로서 단원 테스트는 우리가 작성한 코드의 정확성을 증명할 수 있기 때문에 우리에게 매우 중요하다.더 중요한 것은 단원 테스트는 우리로 하여금 코드 라이브러리에 대해 자신감을 가지고 업데이트를 할 수 있게 하고 우리가 어떤 것도 파괴하지 않았다고 확신하게 한다.그러나 100%의 코드 커버율을 얻기 위해 우리는 업무 테스트가 없을 수도 있는 논리적 컴파일 테스트를 자주 합니다.단원 테스트를 작성하기 위해 아날로그 데이터베이스 추상을 만드는 것은 거의 나쁜 생각이었다고 나는 단언한다.
Qvault 우리는 RESTful Go API 서버 뒤에서 Postgres 데이터베이스를 실행하고 있지만, 우리는 작은 테스트 가능한 함수를 작성하기 위해 최선을 다한다. 그러면 우리는 쓸모없는 시뮬레이션을 작성할 필요가 없다.이런 코드는 저장소를 더럽히고 불필요한 추상을 증가시켜 코드를 더욱 이해하기 어렵게 할 뿐만 아니라 테스트 세트의 건장성에 아무런 가치도 증가하지 않는다.

단원 테스트란 무엇입니까?


In computer programming, unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use.

Wikipedia


Go에서 go test 명령을 사용하여 셀 테스트를 수행할 수 있으며 예제 소스 코드는 다음과 같습니다.
func TestLogPow(t *testing.T) {
    expected := math.Round(math.Log2(math.Pow(7, 8)))
    actual := math.Round(logPow(7, 8, 2))
    if actual != expected {
        t.Errorf("Expected %v, got %v", expected, actual)
    }

    expected = math.Round(math.Log2(math.Pow(10, 11)))
    actual = math.Round(logPow(10, 11, 2))
    if actual != expected {
        t.Errorf("Expected %v, got %v", expected, actual)
    }
}
우리가 테스트한 logPow 함수는 다음과 같습니다.
// logPow calculates log_base(x^y)
// without leaving logspace for each multiplication step
// this makes it take less space in memory
func logPow(expBase float64, pow int, logBase float64) float64 {
    // logb (MN) = logb M + logb N
    total := 0.0
    for i := 0; i < pow; i++ {
        total += logX(logBase, expBase)
    }
    return total
}
단원 테스트의 목표는'단원'이나 코드의 일부분을 테스트하는 것이다.만약 우리가 코드를 많은 작은 테스트 가능한 단원으로 분해할 수 있다면, 우리는 이러한 테스트 세트를 통해 우리의 대부분의 테스트를 자동화할 수 있을 것이다.

좋은 코드는 테스트하기 쉽다

logPow는 일련의 단원 테스트의 완벽한 후보자이다.이것은 수학 함수로 어떤 입력이든 예측 가능한 출력을 가지고 있다.우리가 작성한 모든 함수가 이렇게 간단하게 테스트를 할 수 있는 것은 아니다.그러나 우리가 작고 이상적인 테스트pure functions where possible를 작성할 수 있다면 테스트를 작성하는 것이 훨씬 쉽다.
테스트는 어렵지 않을 거예요.간단한 코드는 테스트하기 쉽다.

단원 테스트는 인프라 시설에 의존해서는 안 된다.


웹 개발에서 각 입출력 지점에서 구조 경계를 만드는 것은 일반적으로 좋은 실천으로 간주된다Clean Code
다음 함수를 보십시오.
func saveUser(db *sql.DB, user *User) error {
    if user.EmailAddress == "" {
        return errors.New("user requires an email")
    }
    if len(user.Password) < 8 {
        return errors.New("user password requires at least 8 characters")
    }
    hashedPassword, err = hash(user.Password)
    if err != nil {
        return err
    }
    _, err := db.Exec(`                                                                                                                          
        INSERT INTO usr (password, email_address, created)                                                                                                                                                                                                                                                                                                                                                                                       
        VALUES ($1, $2, $3);`,
        hashedPassword, user.EmailAddress, time.Now(),
    )
    if err != nil {
        return err
    }
    return nil
}
테스트할 때 사용할 수 있는 데이터베이스 연결이 없으면 이 함수를 테스트할 수 없습니다.만약 새로운 개발자가 이 프로젝트를 복제한다면, 그들은 데이터베이스를 만들어야 한다. 그렇지 않으면 테스트가 실패할 것이다.연속 집적 테스트 파이프를 설정하면 서버에 가상 데이터베이스가 없으면 실패할 수 있습니다.관건은, 이것은 매우 엉망이다.
개발자는 환매 협의를 복제하고 통과된 테스트를 즉시 실행할 수 있어야 한다.
테스트를 실현하기 위해 코드를 복원하는 방법은 테스트 가능한 논리를 분리하는 것이다.예를 들면 다음과 같습니다.
func saveUser(db *sql.DB, user *User) error {
    err := validateUser(user)
    if err != nil{
        return err
    }
    user.Password, err = hash(user.Password)
    if err != nil {
        return err
    }
    if err := saveUserInDB(user); err != nil{
        return err
    }
    return nil
}
이제 사용자를 저장하는 데 사용되는 주요 기능은
  • 사용자 인증
  • 해싱 암호
  • 사용자를 데이터베이스에 저장
  • 우리는 전체 함수를 테스트하기 위해 테스트를 작성할 필요가 없다. 우리는 우리의 관심 부분을 독립적으로 테스트할 수 있다.가령 SQL 함수는 SQL 조회만 할 뿐 단원 테스트가 필요하지 않을 수도 있다.만약 암호 산열 알고리즘이 양호한 테스트를 거친 암호화 라이브러리에 대한 간단한 호출보다 논리를 더 많이 포함한다면 우리는 쉽게 테스트를 작성할 수 있다. 그렇지 않으면 암호화 라이브러리의 테스트를 신뢰할 수 있다.우리가 여기서 필요로 할 수 있는 유일한 테스트는 우리 자신의 업무 논리saveUserInDB 함수를 테스트하는 것이다.
    func TestValidateUser(t *testing.T) {
        err := validateUser(&User{})
        if err == nil {
            t.Error("expected an error")
        }
    
        err := validateUser(&User{
            Email: "[email protected]",
            Password: "thisIsALongEnoughPassword"
        })
        if err != nil {
            t.Error("should have passed")
        }
    }
    
    
    관건은 우리가 쉽게 작성한 테스트를 하려면 대형 함수를 더 작은 봉인 단원으로 분해해야 한다는 것이다.

    당신의 사용자 비밀번호는 안전합니다. 외부 의존 관계도 비웃지 마세요.


    마지막 경험법칙인'단원 테스트는 인프라에 의존해서는 안 된다'는 것은 논란이 없지만 다음은 더욱 격렬한 논쟁거리다.나는 대부분의 데이터베이스와 API 시뮬레이션이 오류 코드의 결과라고 믿는다. 이 문제를 해결하려면 우리가 방금 한 것처럼 더 작은 함수나 단원을 재구성하는 것이 가장 좋다.
    그러나 일부 엔지니어들은 차라리 아날로그 데이터베이스 인터페이스를 만들고 빌어먹을 것 전체를 테스트하는 것이 낫다.
    type sqlDB interface {
        Exec(query string, args ...interface{}) (sql.Result, error)
    }
    
    type mockDB struct {}
    
    func (mdb *mockDB) Exec(query string, args ...interface{}) (sql.Result, error) {
            return nil, nil
    }
    
    func saveUser(db sqlDB, user *User) error {
        if user.EmailAddress == "" {
            return errors.New("user requires an email")
        }
        if len(user.Password) < 8 {
            return errors.New("user password requires at least 8 characters")
        }
        hashedPassword, err := hash(user.Password)
        if err != nil {
            return err
        }
        _, err := db.Exec(`                                                                                                                          
            INSERT INTO usr (password, email_address, created)                                                                                                                                                                                                                                                                                                                                                                                       
            VALUES ($1, $2, $3);`,
            hashedPassword, user.EmailAddress, time.Now(),
        )
        if err != nil {
            return err
        }
        return nil
    }
    
    이 코드가 있으면 우리는 현재 단원 테스트를 작성하여 validateUser에 전송하여 전체saveUser 함수를 호출할 수 있다. 그러면 우리는 실행 중인 데이터베이스에 의존하여 테스트할 필요가 없다.내가 바라는 바와 같이 이런 방법에는 몇 가지 문제가 있다.
  • 아날로그 데이터베이스 코드는 생산 과정에서 사용되지 않습니다.우리는 사실상 중요하지 않은 코드를 테스트하고 있다.
  • 기술적으로 말하자면 우리가 이런 방법을 사용하면 더욱 좋은'테스트 범위'가 있지만 우리의 테스트는 실제적으로 더 건장하지 않다.우리는 거짓된 안정감을 가지고 있다.
  • 우리는 진정한 코드를 인터페이스 뒤로 추상화하여 더욱 찾기 어렵게 할 것이다.
  • mockDB 테스트하기 어려운 이 사실은 우리 개발자에게 재구성이 필요하다는 좋은 신호를 보냈다.우리는 코드를 정리해야 한다는 좋은 신호를 제거했다.

    당신의 의존 관계를 테스트하지 말고, 그것들이 자신의 테스트를 통과했는지 확인하세요


    이전의 재구성saveUser 함수를 예로 들면 우리는 두 가지 함수가 제3자 라이브러리, 즉 saveUser 함수와 hash 함수에 의존할 수 있다.만약 우리가 코드를 잘 썼다면, 이 함수들은 라이브러리 API만 봉인할 것이다.
    func hash(password string) (string, error) {
        const cost = 10
        bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
        return string(bytes), err
    }
    
    나는 saveUserToDB 함수를 테스트할 이유가 없다.hash 라이브러리를 가져오기 전에 저는 직무 조사를 하고 이 코드 라이브러리의 관리자가 좋은 테스트를 작성했는지 확인해야 합니다.일단 내가 그들이 이미 이렇게 했다고 확신한다면, 나는 모든 도출된 함수에 대해 불필요한 테스트를 할 필요가 없다.
    만약 당신이 이 글에 대해 어떤 의문이나 평론이 있다면, 반드시 소셜네트워크서비스(SNS)를 통해 저에게 연락을 주시고, 저에게 알려 주십시오.

    읽어주셔서 감사합니다!


    가지다
    질문이나 댓글이 있으면 팔로우를 하고 트위터에 연락 주세요.
    우리 시사통신을 클릭하여 더 많은 프로그래밍 문장을 얻기

    좋은 웹페이지 즐겨찾기