10. 测试独立的微服务


文章目录

  • 10. 测试独立的微服务
  • 需要测试的范围
  • 需要测试的目标
  • 进行测试的架构
  • 重构项目的 index
  • 将会用到的一些依赖
  • 测试环境配置
  • 第一个测试 测试登录
  • 测试无效输入
  • email 需要是唯一的
  • 在测试期间更改节点环境
  • 测试登录
  • 登出测试
  • 测试时遇到的 cookie 不好传递的问题
  • 认证测试的解决
  • Auth Helper Function
  • 测试没认证的


需要测试的范围

测试的范围是哪些?

Example

单独测试一段代码

独立的 middleware

测试不同的代码片段如何协同工作

从多个中间件到单个请求处理器的请求流(这里用英文更直观,中文属实表达不清晰,Request flowing through multiple middlewares to a request handler)

测试不同组件/模块如何协同工作

向服务发出请求,确保数据库的写入是完成了的

测试不同服务如何协同工作

在“付款 payment”服务中创建“付款”会影响“订单 order”服务

nest 微服务 修改 scripts 微服务 node_测试

⬆ back to top

需要测试的目标

  • 请求的测试

nest 微服务 修改 scripts 微服务 node_nodejs_02

  • 数据库 model 的测试
  • 事件收发的测试

⬆ back to top

进行测试的架构

nest 微服务 修改 scripts 微服务 node_nodejs_03

nest 微服务 修改 scripts 微服务 node_nest 微服务 修改 scripts_04


nest 微服务 修改 scripts 微服务 node_supertest_05


nest 微服务 修改 scripts 微服务 node_jest_06

⬆ back to top

重构项目的 index

// app.ts
import express from "express";
import "express-async-errors";
import { json } from "body-parser";
import cookieSession from "cookie-session";

import { currentUserRouter } from "./routes/current-user";
import { signinRouter } from "./routes/signin";
import { signoutRouter } from "./routes/signout";
import { signupRouter } from "./routes/signup";
import { errorHandler } from "./middlewares/error-handler";
import { NotFoundError } from "./errors/not-found-error";

const app = express();
app.set("trust proxy", true);
app.use(json());
app.use(
  cookieSession({
    signed: false,
    secure: true,
  })
);

app.use(currentUserRouter);
app.use(signinRouter);
app.use(signoutRouter);
app.use(signupRouter);

app.all("*", async (req, res) => {
  throw new NotFoundError();
});

app.use(errorHandler);

export { app };
// index.ts
import mongoose from "mongoose";

import { app } from "./app";

const start = async () => {
  if (!process.env.JWT_KEY) {
    throw new Error("JWT_KEY must be defined");
  }

  try {
    await mongoose.connect("mongodb://auth-mongo-srv:27017/auth", {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
    });
    console.log("Connected to MongoDb");
  } catch (err) {
    console.log(err);
  }

  app.listen(3000, () => {
    console.log("Listening on port 3000!");
  });
};

start();

⬆ back to top

将会用到的一些依赖

jestsupertestmongodb-memory-server

⬆ back to top

测试环境配置

// package.json
  "scripts": {
    "start": "ts-node-dev --poll src/index.ts", 
    "test": "jest --watchAll --no-cache" // --watchAll 观察项目所有测试文件的变化  --no-cache 为了解决 jest 有时候识别不了 TypeScript 的文件变化的问题
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "setupFilesAfterEnv": [
      "./src/test/setup.ts"
    ]
  },
// ./test/setup.ts
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import { app } from "../app";

let mongo: any;
beforeAll(async () => {
  process.env.JWT_KEY = "asdfasdf";

  mongo = new MongoMemoryServer();
  const mongoUri = await mongo.getUri();

  await mongoose.connect(mongoUri, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
});

beforeEach(async () => {
  // 在进行每一个单元测试之前,都要清空每一个 connection 的数据
  const collections = await mongoose.connection.db.collections();

  for (let collection of collections) {
    await collection.deleteMany({});
  }
});

afterAll(async () => {
  await mongo.stop();
  await mongoose.connection.close();
});

⬆ back to top

第一个测试 测试登录

// signup.test.ts
import request from "supertest";
import { app } from "../../app";

it("returns a 201 on successful signup", async () => {
  return request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);
});
npm run test

nest 微服务 修改 scripts 微服务 node_nodejs_07

  • 这里报错是因为测试环境里没加 JWT
  • 之前我们是在 每个 pod 里kubectl create secret generic jwt-secret --from-literal=JWT_KEY=xxxxxx
  • 所以需要在 beforeAll 里加 JWT 假装有 JWT 即可
  • process.env.JWT_KEY = "asdfasdf";

⬆ back to top

测试无效输入

// signup.test.ts
import request from "supertest";
import { app } from "../../app";

it("returns a 400 with an invalid email", async () => {
  return request(app)
    .post("/api/users/signup")
    .send({
      email: "alskdflaskjfd",
      password: "password",
    })
    .expect(400);
});

it("returns a 400 with an invalid password", async () => {
  return request(app)
    .post("/api/users/signup")
    .send({
      email: "alskdflaskjfd",
      password: "p",
    })
    .expect(400);
});

it("returns a 400 with missing email and password", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
    })
    .expect(400);

  await request(app)
    .post("/api/users/signup")
    .send({
      password: "alskjdf",
    })
    .expect(400);
});

⬆ back to top

email 需要是唯一的

// signup.test.ts
it("disallows duplicate emails", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);

  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(400);
});

⬆ back to top

在测试期间更改节点环境

// signup.test.ts
it("sets a cookie after successful signup", async () => {
  const response = await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);
  // 测试 cookie 有没有 set 进去,有 cookie 就是 define 的
  expect(response.get("Set-Cookie")).toBeDefined();
});

nest 微服务 修改 scripts 微服务 node_nest 微服务 修改 scripts_08

  • 为什么会出现这种情况?
  • 在我们 cookie 的配置中,secure 写的是 true,那么就会开启 https
  • supertest 用的是 http 不是 https
  • 所以需要根据当前 process.env.NODE_ENV 来判断要不要开 http 和 https
app.use(
  cookieSession({
    signed: false,
    secure: process.env.NODE_ENV !== 'test'
  })
);

⬆ back to top

测试登录

// signin.test.ts
import request from "supertest";
import { app } from "../../app";

it("fails when a email that does not exist is supplied", async () => {
  await request(app)
    .post("/api/users/signin")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(400);
});

it("fails when an incorrect password is supplied", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);

  await request(app)
    .post("/api/users/signin")
    .send({
      email: "test@test.com",
      password: "aslkdfjalskdfj",
    })
    .expect(400);
});

it("responds with a cookie when given valid credentials", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);

  const response = await request(app)
    .post("/api/users/signin")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(200);

  expect(response.get("Set-Cookie")).toBeDefined();
});

⬆ back to top

登出测试

  • 我们在登出的时候,直接把 session 设为 null 了
router.post('/api/users/signout', (req, res) => {
  req.session = null;

  res.send({});
});
// signout.test.ts
import request from "supertest";
import { app } from "../../app";

it("clears the cookie after signing out", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);

  const response = await request(app)
    .post("/api/users/signout")
    .send({})
    .expect(200);

  expect(response.get("Set-Cookie")[0]).toEqual(
    "express:sess=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly"
  );
});
  • 测试小技巧,打印正确的 response,然后测试 response 即可

    ⬆ back to top

测试时遇到的 cookie 不好传递的问题

  • 很正常的一个 测试,我们希望登录然后查看 currentuser
  • 然后不出意外确实是可以看到 response body 中的 currentuser
// current-user.test.ts
import request from "supertest";
import { app } from "../../app";

it("responds with details about the current user", async () => {
  await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);

  const response = await request(app)
    .get("/api/users/currentuser")
    .send()
    .expect(200);

  console.log(response.body);
});
  • 但是打印出来的是 current null
  • 就说明获取不了 我们的登录状态

⬆ back to top

认证测试的解决

  • 因为我们登录状态 session 会话(包含 JWT),是通过 cookie 在客户端里存的,还记得之前 express-session 和 cookie-session 的测试吗?
  • 所以要获取到存登录状态的 cookie,必须在登录的时候获取 response 的 cookie,缓存下来,在验证 currentuser 的时候加上
import request from "supertest";
import { app } from "../../app";

it("responds with details about the current user", async () => {
  const authResponse = await request(app)
    .post("/api/users/signup")
    .send({
      email: "test@test.com",
      password: "password",
    })
    .expect(201);
  const cookie = authResponse.get("Set-Cookie");

  const response = await request(app)
    .get("/api/users/currentuser")
    .set("Cookie", cookie)
    .send()
    .expect(200);

  expect(response.body.currentUser.email).toEqual("test@test.com");
});

⬆ back to top

Auth Helper Function

  • 因为我们之后都希望测试的时候,能拿到会话的cookie
  • 所以可以将这段代码抽出来复用
global.signin = async () => {
  const email = "test@test.com";
  const password = "password";

  const response = await request(app)
    .post("/api/users/signup")
    .send({
      email,
      password,
    })
    .expect(201);

  const cookie = response.get("Set-Cookie");

  return cookie;
};

⬆ back to top

测试没认证的

it("responds with null if not authenticated", async () => {
  const response = await request(app)
    .get("/api/users/currentuser")
    .send()
    .expect(200);

  expect(response.body.currentUser).toEqual(null);
});

⬆ back to top