Jest로 Express 테스트하기

Jest로 Express API 유닛 테스트를 할 때 겪었던 문제를 다른 사람들도 안 겪었으면 하는 바램에서 글을 썼다. 원래 유닛 테스트는 MochaChai를 사용했는데 이번에 한 번 Jest로 테스트 프레임워크를 바꿔보았다.

Jest를 사용하기 위해서는 일단 Install.

1
npm install --save-dev babel-cli, babel-preset-env, jest, supertest

아래는 간단한 웹 서버 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.js
import express from 'express';

const app = express();
const port = 3000;

app.get('/hello', (req, res) => {
res.send('world!');
});

app.get('/status', (req, res) => {
res.send('ok');
});

app.listen(port);

module.exports = app;

매우 간단한 이 웹 서버는 두 개의 라우트(/hello, /status)밖에 없다. /hello 라우트는 단순하게 “world!” 문자열만 리턴하며 /status는 단순하게 “ok” 문자열만 리턴한다.

이제 Jest와 Supertest를 사용해서 이 라우트를 테스트를 해보겠다.

먼저 /hello

1
2
3
4
5
6
7
8
9
10
11
12
// tests/hello.test.js
import request from 'supertest';
import app from '../app';

describe('Test /hello', () => {
it ('should return world!', (done) => {
request(app).get('/hello').then((response) => {
expect(response.text).toBe('world!');
done();
});
});
});

Test!

깔끔하게 성공한 것을 볼 수 있다.

그럼 이제 /status에 대한 테스트를 해보자. 이 테스트는 다른 파일에 작성하도록 한다. 물론 식은 죽 먹기다.

1
2
3
4
5
6
7
8
9
10
11
12
// tests/status.test.js
import request from 'supertest';
import app from '../app';

describe('Test /status', () => {
it ('should return ok', (done) => {
request(app).get('/status').then((response) => {
expect(response.text).toBe('ok');
done();
});
});
});

Test! 🔥

😱 당연히 성공할 줄 알았던 테스트가 EADDRINUSE 에러를 내면서 실패했다. 문제는 첫 번째 테스트를 실행하면서 서버는 아직도 3000번 포트를 listen 하고 있기 때문이다. 그래서 두 번째 테스트에서 다시 import app from ‘../app’ 을 실행하면 이전 테스트에서 이미 listen하고 있는 3000번 포트를 다시 사용하기 때문에 문제가 발생한 것이었다.

이 문제를 해결하기 위해서는 테스트가 아닐 경우에만 3000번 포트를 열도록 변경해야 한다. 어차피 테스트 환경일 경우에는 Supertest를 통해서 서버를 구동하기 때문에 네트워크 포트를 열 필요가 없다. 따라서 package.json을 아래와 같이 바꾼다.

1
2
3
4
5
{
"scripts": {
"unit": "NODE_ENV=test jest tests/*.test.js --forceExit",
}
}

그리고 app.js를 아래와 같이 바꾼다.

1
2
3
4
// ...
if (process.env.NODE_ENV !== 'test') {
app.listen(port);
}

다시 한 번 실행하면

성공!

go 언어 구글 클라우드 Datastore 패키지를 잘 씁시다!

3월 초에 시간이 좀 많이 나서 펫 프로젝트를 진행한 일이 있다. 구글 클라우드에서 AppEngine Standard Environment + Datastore으로 프로젝트를 구성했다. AppEngine은 복습할 겸 회사에서 지겹도록 파이썬을 써서 Go언어로 만들었다. API는 쉽게 구현할 수 있었다. 다만 문제는 속도가 너무 느렸다. 아래는 대략적인 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package mypetproject

import (
"context"
"net/http"

"github.com/gorilla/mux"
"cloud.google.com/go/datastore"
"google.golang.org/appengine"
)

func handleTable(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)

dsClient, err := datastore.NewClient(ctx, "my-project")
if err != nil {
// Handle error.
}
k := datastore.NameKey("Entity", "stringID", nil)
e := new(Entity)
if err := dsClient.Get(ctx, k, e); err != nil {
// Handle error.
}
// ...
}

func init() {
r := mux.NewRouter()
r.HandleFunc("/table", handleTable).Methods("GET")

http.Handle("/", r)
}

하나의 Kind(관계형DB의 Table)에 몇 개 되지도 않은 Entity(관계형DB의 Row)만 있고 단순한 GET 만을 했을 뿐인데 속도가 600~800ms 나 걸렸다! 제일 의심이 가던 부분은 datastore.NewClient()였다. 이 함수에서 Datastore에 연결하는데 오래 걸리리라 생각했다. 그래서 이를 패키지 전역으로 빼려고 했으나 실패. datastore.NewClient()의 인자로 context를 받는데, 이 context는 appengine의 context 를 받아야 했다.

1
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error)

구글이 이렇게 허접하게 만들었을리 없는데, 뭔가 다른 방법이 있지 않을까 하고 낑낑대던 와중에 눈에 띈 패키지명이 있었다. 바로 google.golang.org/appengine/datastore … 내가 사용했던 Datastore 패키지와 달랐다.

정리하자면

google.golang.org/appengine/datastore는 Google App Engine Standard Environment에서 사용하는 패키지다. godoc에서 확인해보면 맨 첫 머리에 아래와 같은 설명이 있다.

Package datastore provides a client for App Engine’s datastore service.

그렇다. google.golang.org/appengine/datastore 패키지는 App Engine Standard Environment 전용이며, 그 외에 로컬이나 Compute Engine, App Engine Flexible Environment에서 사용할 때는 cloud.google.com/go/datastore 를 사용해야 한다.

App Engine에서 부를때는 Client를 만드는 부분, 즉 인증, 연결 부분이 생략되어 있기 때문에 코드가 다음과 같이 조금 더 간단해졌다. 또한 응답시간 역시 기존 600~800ms에서 모두 80ms 아래로 떨어뜨릴 수 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package mypetproject

import (
"context"
"net/http"

"github.com/gorilla/mux"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
)

func handleTable(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)

k := datastore.NewKey(ctx, "Entity", "stringID", 0, nil)
e := new(Entity)
if err := datastore.Get(ctx, k, e); err != nil {
http.Error(w, err.Error(), 500)
return
}
// ...
}

func init() {
r := mux.NewRouter()
r.HandleFunc("/table", handleTable).Methods("GET")

http.Handle("/", r)
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×