PowerMock: 支援GRPC協議的 Mock Server

公子扶搖發表於2021-07-06

PowerMock是一個Mock Server的實現,它同時支援HTTP與gRPC協議介面的Mock,並提供了靈活的外掛功能。
這個工具面向於前後端、測試等對有介面Mock需求的開發人員,也可以作為一個通用的Mock服務,部署在閘道器架構或API管理平臺中,實現降級、介面Mock等功能。

專案地址

專案地址:PowerMock

功能

作為一個Mock Server,PowerMock具有以下的核心功能:

  1. 支援 HTTP協議gRPC協議 介面的Mock。
  2. 支援配置 Javascript 等指令碼語言來動態生成響應。
  3. 支援對一個介面配置多種響應,並按照條件進行區分。
  4. 匹配條件支援多種運算子(AND/OR/>/</=等)。
  5. 支援返回靜態資料以及 特定領域的隨機資料
  6. 支援 外掛 功能,可以通過編寫外掛實現其他匹配或Mock引擎。
  7. 同時提供HTTP與gRPC介面,可以動態對MockAPI進行 增刪改查
  8. 開箱即用的Redis儲存,並支援自由擴充其他儲存引擎,比如MySQL、etcd。
  9. 同時支援 windows / darwin / linux 的 32 位 與 64 位。
  10. 語言無關,任何使用HTTP協議或gRPC協議的專案均可以使用本工具。

示例

一、較為高階的用法

本示例可以在 示例程式碼 找到對應資料
本示例必須使用v8版本的powermock,才能完整支援Javascript的功能

以下面這份配置為示例:

uniqueKey: "advanced_example"
path: "/examples.greeter.api.Greeter/Hello"
method: "POST"
cases:
  - condition:
      simple:
        items:
          - operandX: "$request.header.uid"
            operator: "<="
            operandY: "1000"
    response:
      simple:
        header:
          x-unit-id: "3"
          x-unit-region: "sh"
        trailer:
          x-api-version: "1.3.2"
        body: |
          {"timestamp": "1111", "message": "This message will only be returned when uid <= 1000", "amount": "{{ $mock.price }}"}
  - condition:
      simple:
        items:
          - operandX: "$request.header.uid"
            operator: ">"
            operandY: "1000"
    response:
      script:
        lang: "javascript"
        content: |
          (function(){
              function random(min, max){
                  return parseInt(Math.random()*(max-min+1)+min,10);
              }
              return {
                  code: 0,
                  header: {
                      "x-unit-id": (request.header["uid"] % 5).toString(),
                      "x-unit-region": "bj",
                  },
                  trailer: {
                      "x-api-version": "1.3.2",
                  },
                  body: {
                      timestamp: Math.ceil(new Date().getTime() / 1000),
                      message: "this message is generated by javascript, your uid is: " + request.header["uid"],
                      amount: random(0, 5000),
                  },
              }
          })()

這份配置定義了一個MockAPI,用於匹配所有路徑為 /examples.greeter.api.Greeter/Hello,方法為 POST 的請求,它包含了兩個場景,能夠實現這樣的效果:

1. 條件場景一

當請求 Header 中的 uid <= 1000 時:

  • Response Header 中寫入:
x-unit-id: "3"
x-unit-region: "sh"
  • Response Trailer 中寫入:
x-api-version: "1.3.2"
  • Response Body 中寫入:
{"timestamp": "1111", "message": "This message will only be returned when uid <= 1000", "amount": "{{ $mock.price }}"}

其中的 {{ $mock.price }} 是魔法變數,用於返回一個隨機的價格資料。最終,客戶端收到的 Response Body 類似於:

{
	"timestamp": "1111",
	"message": "This message will only be returned when uid <= 1000",
	"amount": 7308.4
}

2. 條件場景二

當請求 Header 中的 uid > 1000 時,通過執行以下Javascript指令碼返回響應:

(function(){
    function random(min, max){
        return parseInt(Math.random()*(max-min+1)+min,10);
    }
    return {
        code: 0,
        header: {
            "x-unit-id": (request.header["uid"] % 5).toString(),
            "x-unit-region": "bj",
        },
        trailer: {
            "x-api-version": "1.3.2",
        },
        body: {
            timestamp: Math.ceil(new Date().getTime() / 1000),
            message: "this message is generated by javascript, your uid is: " + request.header["uid"],
            amount: random(0, 5000),
        },
    }
})()

在這個指令碼中,根據請求的 Header,以及一些內建或自定義函式來生成了響應的code、header、trailer與body。
最終客戶端收到的響應體類似於:

{
	"timestamp": 1622093545,
	"message": "this message is generated by javascript, your uid is: 2233",
	"amount": 314
}

它描述了一個相對複雜的場景,當然可能你的需求比較簡單,實戰的話,我們先從Hello World開始吧!

二、從Hello World開始吧

本示例可以在 示例程式碼 找到對應資料

首先,建立一個配置檔案:

log:
    pretty: true
    level: debug
grpcmockserver:
    enable: true
    address: 0.0.0.0:30002
    protomanager:
        protoimportpaths: [ ]
        protodir: ./apis
httpmockserver:
    enable: true
    address: 0.0.0.0:30003
apimanager:
    grpcaddress: 0.0.0.0:30000
    httpaddress: 0.0.0.0:30001
pluginregistry: { }
plugin:
    simple: { }
    grpc: { }
    http: { }
    script: { }
    redis:
        enable: false
        addr: 127.0.0.1:6379
        password: ""
        db: 0
        prefix: /powermock/

將編譯好的PowerMock與上面建立好的配置檔案放到同一個目錄中,像下面這樣:

➜ ls -alh
total 45M
drwxrwxrwx 1 storyicon storyicon 4.0K May 27 14:18 .
drwxrwxrwx 1 storyicon storyicon 4.0K May 24 11:43 ..
-rwxrwxrwx 1 storyicon storyicon  546 May 27 14:16 config.yaml
-rwxrwxrwx 1 storyicon storyicon  45M May 27 14:18 powermock

然後執行

➜ ./powermock serve --config.file config.yaml

如果沒有埠衝突的話,你應該已經可以看到服務執行起來了!

1. 先Mock一個HTTP介面

在上面的目錄下,建立一個名為 apis.yaml 的檔案:

uniqueKey: "hello_example_http"
path: "/hello"
method: "GET"
cases:
    - response:
          simple:
              header:
                  x-unit-id: "3"
                  x-unit-region: "sh"
              trailer:
                  x-api-version: "1.3.2"
              body: |
                  hello world!

然後執行:

➜ ./powermock load --address=127.0.0.1:30000 apis.yaml
2:32PM INF start to load file component=main file=load.go:59
2:32PM INF mock apis loaded from file component=main count=1 file=load.go:64
2:32PM INF start to save api component=main file=load.go:76 host= method=GET path=/hello uniqueKey=hello
2:32PM INF succeed! component=main file=load.go:89

這樣,我們描述的MockAPI就建立起來了。

通過 curl 或者你的瀏覽器請求 http://127.0.0.1:30003/hello,可以看到返回給我們 hello world 了!

➜ curl http://127.0.0.1:30003/hello -i
HTTP/1.1 200 OK
Content-Type: application/json
X-Unit-Id: 3
X-Unit-Region: sh
Date: Thu, 27 May 2021 06:36:28 GMT
Content-Length: 12

hello world!

2. 再mock一個gRPC介面

在上面的目錄中,建立一個 apis 目錄,使整個目錄結構像下面這樣:

➜  ls -alh
total 45M
drwxrwxrwx 1 storyicon storyicon 4.0K May 27 14:42 .
drwxrwxrwx 1 storyicon storyicon 4.0K May 27 14:37 ..
drwxrwxrwx 1 storyicon storyicon 4.0K May 27 14:23 apis
-rwxrwxrwx 1 storyicon storyicon 1.8K May 27 14:32 apis.yaml
-rwxrwxrwx 1 storyicon storyicon  546 May 27 14:16 config.yaml
-rwxrwxrwx 1 storyicon storyicon  45M May 27 14:18 powermock

在 apis 目錄中建立我們的 greeter.proto:

syntax = "proto3";

package examples.greeter.api;
option go_package = "github.com/bilibili-base/powermock/examples/helloWorld/apis;apis";

service Greeter {
    rpc Hello(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string message = 2;
}

message HelloResponse {
    string message = 2;
}

現在整個目錄結構像這樣:

.
├── apis
│   └── greeter.proto
├── apis.yaml
├── config.yaml
└── powermock

重新執行我們的 powermock 來載入我們新寫的proto檔案:

➜ ./powermock serve --config.file config.yaml
2:55PM INF starting load proto from: ./apis component=main.gRPCMockServer.protoManager file=service.go:102
2:55PM INF api loaded component=main.gRPCMockServer.protoManager file=service.go:131 name=/examples.greeter.api.Greeter/Hello

在啟動日誌中可以看到我們新建立的 proto 檔案已經被載入到 PowerMock 中了。

將我們的 apis.yaml 檔案修改成下面的內容:

uniqueKey: "hello_example_http"
path: "/hello"
method: "GET"
cases:
    - response:
          simple:
              header:
                  x-unit-id: "3"
                  x-unit-region: "sh"
              trailer:
                  x-api-version: "1.3.2"
              body: |
                  hello world!

---

uniqueKey: "hello_example_gRPC"
path: "/examples.greeter.api.Greeter/Hello"
method: "POST"
cases:
    - response:
          simple:
              header:
                  x-unit-id: "3"
                  x-unit-region: "sh"
              trailer:
                  x-api-version: "1.3.2"
              body: |
                  {"message": "hello world!"}

可以看到,裡面新增了一個名為 "hello_example_gRPC" 的 MockAPI,我們通過下面的命令裝載它:

➜ powermock load --address=127.0.0.1:30000  apis.yaml
3:06PM INF start to load file component=main file=load.go:59
3:06PM INF mock apis loaded from file component=main count=2 file=load.go:64
3:06PM INF start to save api component=main file=load.go:76 host= method=GET path=/hello uniqueKey=hello_example_http
3:06PM INF start to save api component=main file=load.go:76 host= method=POST path=/examples.greeter.api.Greeter/Hello uniqueKey=hello_example_gRPC
3:06PM INF succeed! component=main file=load.go:89

這樣,我們的MockAPI就被新增到PowerMock中了。

如果你的環境中有BloomRPC之類的工具的話,可以先通過BloomRPC載入 greeter.proto,然後呼叫 127.0.0.1:30002

可以看到,呼叫成功返回了 "hello world"。
如果使用程式語言進行呼叫的話,以 golang 為例,通過下面的程式碼呼叫 PowerMock:

func main() {
	fmt.Println("starting call mock server")
	conn, err := grpc.Dial("127.0.0.1:30002", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	client := apis.NewGreeterClient(conn)

	var header, trailer metadata.MD
	startTime := time.Now()
	resp, err := client.Hello(context.TODO(), &apis.HelloRequest{
		Message: "hi",
	}, grpc.Header(&header), grpc.Trailer(&trailer))
	if err != nil {
		panic(err)
	}
	fmt.Printf("[elapsed] %d ms \r\n", time.Since(startTime).Milliseconds())
	fmt.Printf("[headers] %+v \r\n", header)
	fmt.Printf("[trailer] %+v \r\n", trailer)
	fmt.Printf("[response] %+v \r\n", resp.String())
}

日誌輸出是這樣的:

starting call mock server
[elapsed] 2 ms
[headers] map[content-type:[application/grpc] x-unit-id:[3] x-unit-region:[sh]]
[trailer] map[x-api-version:[1.3.2]]
[response] message:"This message will only be returned when uid <= 1000"

可以看到,我們的介面被成功Mock出來了!

安裝

通過Go安裝

安裝普通版本,無Javascript支援:

go install github.com/bilibili-base/powermock/cmd/powermock

安裝V8版本,支援Javascript:

go install github.com/bilibili-base/powermock/cmd/powermock-v8

開箱即用版本

如果你沒有定製外掛的需求,開箱即用版本 非常適合你。

通過Makefile編譯

如果你是 linux/darwin/wsl 的使用者,推薦使用 makefile 來進行安裝:

➜ git clone https://github.com/bilibili-base/powermock
➜ cd powermock
➜ make build_linux_v8
➜ make build_linux
➜ make build_darwin
➜ make build_windows

當然也可以直接進行編譯:

➜ cd ./cmd/powermock
➜ go install
➜ go build .

相關文章