跳转到主要内容

标签(标签)

资源精选(342) Go开发(108) Go语言(103) Go(99) angular(82) LLM(75) 大语言模型(63) 人工智能(53) 前端开发(50) LangChain(43) golang(43) 机器学习(39) Go工程师(38) Go程序员(38) Go开发者(36) React(33) Go基础(29) Python(24) Vue(22) Web开发(20) Web技术(19) 精选资源(19) 深度学习(19) Java(18) ChatGTP(17) Cookie(16) android(16) 前端框架(13) JavaScript(13) Next.js(12) 安卓(11) typescript(10) 资料精选(10) NLP(10) 第三方Cookie(9) Redwoodjs(9) LLMOps(9) Go语言中级开发(9) 自然语言处理(9) 聊天机器人(9) PostgreSQL(9) 区块链(9) mlops(9) 安全(9) 全栈开发(8) ChatGPT(8) OpenAI(8) Linux(8) AI(8) GraphQL(8) iOS(8) 软件架构(7) Go语言高级开发(7) AWS(7) C++(7) 数据科学(7) whisper(6) Prisma(6) 隐私保护(6) RAG(6) JSON(6) DevOps(6) 数据可视化(6) wasm(6) 计算机视觉(6) 算法(6) Rust(6) 微服务(6) 隐私沙盒(5) FedCM(5) 语音识别(5) Angular开发(5) 快速应用开发(5) 提示工程(5) Agent(5) LLaMA(5) 低代码开发(5) Go测试(5) gorm(5) REST API(5) 推荐系统(5) WebAssembly(5) GameDev(5) CMS(5) CSS(5) machine-learning(5) 机器人(5) 游戏开发(5) Blockchain(5) Web安全(5) Kotlin(5) 低代码平台(5) 机器学习资源(5) Go资源(5) Nodejs(5) PHP(5) Swift(5) 智能体(4) devin(4) Blitz(4) javascript框架(4) Redwood(4) GDPR(4) 生成式人工智能(4) Angular16(4) Alpaca(4) SAML(4) JWT(4) JSON处理(4) Go并发(4) kafka(4) 移动开发(4) 移动应用(4) security(4) 隐私(4) spring-boot(4) 物联网(4) nextjs(4) 网络安全(4) API(4) Ruby(4) 信息安全(4) flutter(4) 专家智能体(3) Chrome(3) CHIPS(3) 3PC(3) SSE(3) 人工智能软件工程师(3) LLM Agent(3) Remix(3) Ubuntu(3) GPT4All(3) 软件开发(3) 问答系统(3) 开发工具(3) 最佳实践(3) RxJS(3) SSR(3) Node.js(3) Dolly(3) 移动应用开发(3) 编程语言(3) 低代码(3) IAM(3) Web框架(3) CORS(3) 基准测试(3) Go语言数据库开发(3) Oauth2(3) 并发(3) 主题(3) Theme(3) earth(3) nginx(3) 软件工程(3) azure(3) keycloak(3) 生产力工具(3) gpt3(3) 工作流(3) C(3) jupyter(3) 认证(3) prometheus(3) GAN(3) Spring(3) 逆向工程(3) 应用安全(3) Docker(3) Django(3) R(3) .NET(3) 大数据(3) Hacking(3) 渗透测试(3) C++资源(3) Mac(3) 微信小程序(3) Python资源(3) JHipster(3) 大型语言模型(2) 语言模型(2) 可穿戴设备(2) JDK(2) SQL(2) Apache(2) Hashicorp Vault(2) Spring Cloud Vault(2) Go语言Web开发(2) Go测试工程师(2) WebSocket(2) 容器化(2) AES(2) 加密(2) 输入验证(2) ORM(2) Fiber(2) Postgres(2) Gorilla Mux(2) Go数据库开发(2) 模块(2) 泛型(2) 指针(2) HTTP(2) PostgreSQL开发(2) Vault(2) K8s(2) Spring boot(2) R语言(2) 深度学习资源(2) 半监督学习(2) semi-supervised-learning(2) architecture(2) 普罗米修斯(2) 嵌入模型(2) productivity(2) 编码(2) Qt(2) 前端(2) Rust语言(2) NeRF(2) 神经辐射场(2) 元宇宙(2) CPP(2) 数据分析(2) spark(2) 流处理(2) Ionic(2) 人体姿势估计(2) human-pose-estimation(2) 视频处理(2) deep-learning(2) kotlin语言(2) kotlin开发(2) burp(2) Chatbot(2) npm(2) quantum(2) OCR(2) 游戏(2) game(2) 内容管理系统(2) MySQL(2) python-books(2) pentest(2) opengl(2) IDE(2) 漏洞赏金(2) Web(2) 知识图谱(2) PyTorch(2) 数据库(2) reverse-engineering(2) 数据工程(2) swift开发(2) rest(2) robotics(2) ios-animation(2) 知识蒸馏(2) 安卓开发(2) nestjs(2) solidity(2) 爬虫(2) 面试(2) 容器(2) C++精选(2) 人工智能资源(2) Machine Learning(2) 备忘单(2) 编程书籍(2) angular资源(2) 速查表(2) cheatsheets(2) SecOps(2) mlops资源(2) R资源(2) DDD(2) 架构设计模式(2) 量化(2) Hacking资源(2) 强化学习(2) flask(2) 设计(2) 性能(2) Sysadmin(2) 系统管理员(2) Java资源(2) 机器学习精选(2) android资源(2) android-UI(2) Mac资源(2) iOS资源(2) Vue资源(2) flutter资源(2) JavaScript精选(2) JavaScript资源(2) Rust开发(2) deeplearning(2) RAD(2)

介绍


本教程将说明如何在 Go 中构建由 PostgreSQL 支持的 REST API,使用 Gorilla Mux 进行路由。本教程将采用测试驱动开发,最后将解释如何在开发过程中对数据库进行持续测试。

目标


在本教程结束时,您将:

  • 熟悉 Gorilla Mux,并且
  • 了解如何使用持续集成 (CI) 针对数据库测试您的应用程序。

先决条件


本教程假设:

  • 基本熟悉 Go 和 PostgreSQL,以及
  • 你有工作的 Go 和 PostgreSQL 安装。您可以使用 Docker 轻松运行测试数据库。

您将在此存储库中找到演示的完整代码。

TomFern/go-mux-api


应用程序简介


在深入了解细节之前,让我们先简要了解一下我们将在本教程中构建的示例应用程序。

应用程序会做什么?


该应用程序将是一个简单的 REST API 服务器,它将公开端点以允许访问和操作“产品”。我们的端点将允许的操作包括:

  • 创造新产品,
  • 更新现有产品,
  • 删除现有产品,
  • 获取现有产品,以及
  • 获取产品列表。

API规范


具体来说,我们的应用程序应该:

  • 创建新产品以响应 /product 上的有效 POST 请求,
  • 更新产品以响应 /product/{id} 处的有效 PUT 请求,
  • 删除产品以响应 /product/{id} 处的有效 DELETE 请求,
  • 获取产品以响应 /product/{id} 处的有效 GET 请求,以及
  • 获取产品列表以响应 /products 上的有效 GET 请求。

上面某些端点中的 {id} 将确定请求将使用哪个产品。

有了这些要求,让我们开始设计我们的应用程序。

创建应用程序结构


在本节中,我们将创建最小的应用程序结构,作为编写测试和进一步开发应用程序的起点。

创建数据库结构


在这个简单的应用程序中,我们将有一个名为 products 的表。该表将包含以下字段:

  • id - 此表中的主键,
  • name  - 产品的名称,以及,
  • price ——产品的价格。

我们可以使用下面的 SQL 语句来创建表:

CREATE TABLE products
(
    id SERIAL,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL DEFAULT 0.00,
    CONSTRAINT products_pkey PRIMARY KEY (id)
)


这是一个最小且非常简单的表格,但它应该足以帮助实现本教程的目标。

获取依赖项


在开始编写应用程序之前,我们需要获取应用程序将依赖的两个包:

  1. mux – Gorilla Mux 路由器(也称为“HTTP 请求多路复用器”,它使 Gorilla 成为最强大的 Go 库之一),并且,
  2. pq – PostgreSQL 驱动程序。

在此之前,让我们在 GitHub 中创建一个存储库来存储我们的代码:

  • 前往 GitHub 并登录或注册。
  • 创建一个新的存储库。
  • 选择 Go 作为语言
  • 通过单击克隆或下载来获取存储库地址。
  • 将存储库克隆到您的计算机:
$ git clone YOUR_REPO_URL
$ cd YOUR_REPO_DIRECTORY


使用您的 GitHub 存储库地址初始化 Go 模块:

$ go mod init github.com/<your GitHub username>/<project name>


您可以使用以下命令获取 Go 模块。

$ go get -u github.com/gorilla/mux
$ go get -u github.com/lib/pq


如果您使用其他机制来供应外部依赖项,请随意以适合您的方式获取和组织这些依赖项。例如,在 Go 参考文档中,您会找到使用 dep 的示例。

搭建一个最小的应用程序


在我们编写测试之前,我们需要创建一个可以用作测试基础的最小应用程序。当我们完成本教程时,我们将拥有以下文件结构。

┌── app.go
├── main.go
├── main_test.go
├── model.go
├── go.sum
└── go.mod


让我们首先定义一个结构 App 来保存我们的应用程序:

type App struct {
    Router *mux.Router
    DB     *sql.DB
}


此结构公开对应用程序使用的路由器和数据库的引用。为了有用和可测试,App 将需要两个方法来初始化和运行应用程序。

这些方法将具有以下签名:

func (a *App) Initialize(user, password, dbname string) { }

func (a *App) Run(addr string) { }

Initialize 方法将获取连接到数据库所需的详细信息。它将创建一个数据库连接并连接路由以根据要求进行响应。

Run 方法将简单地启动应用程序。

我们将把它放在 app.go 中,在这个阶段应该包含以下内容:

// app.go

package main

import (
    "database/sql"

    "github.com/gorilla/mux"
    _ "github.com/lib/pq"
)

type App struct {
    Router *mux.Router
    DB     *sql.DB
}

func (a *App) Initialize(user, password, dbname string) { }

func (a *App) Run(addr string) { }

请注意,我们在这里导入了 pq,因为我们需要我们的应用程序与 PostgreSQL 一起工作。

我们还将创建 main.go,其中将包含我们应用程序的入口点。它应该包含以下代码:

// main.go

package main

import "os"

func main() {
    a := App{}
    a.Initialize(
        os.Getenv("APP_DB_USERNAME"),
        os.Getenv("APP_DB_PASSWORD"),
        os.Getenv("APP_DB_NAME"))

    a.Run(":8010")
}

这假设您使用环境变量 APP_DB_USERNAME、APP_DB_PASSWORD 和 APP_DB_NAME 来分别存储数据库的用户名、密码和名称。

我们将使用 PostgreSQL 默认参数进行测试:

export APP_DB_USERNAME=postgres
export APP_DB_PASSWORD=
export APP_DB_NAME=postgres


我们还需要另一个结构来表示“产品”。让我们定义如下:

type product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

我们可以将处理单个产品的函数定义为该结构上的方法,如下所示:

func (p *product) getProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) updateProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) deleteProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) createProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

我们还将定义一个获取产品列表的独立函数,如下所示:

 

func getProducts(db *sql.DB, start, count int) ([]product, error) {
  return nil, errors.New("Not implemented")
}


将以上所有代码组合到一个文件 model.go 中,您应该会得到类似于以下内容的内容:

// model.go

package main

import (
    "database/sql"
    "errors"
)

type product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func (p *product) getProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) updateProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) deleteProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func (p *product) createProduct(db *sql.DB) error {
  return errors.New("Not implemented")
}

func getProducts(db *sql.DB, start, count int) ([]product, error) {
  return nil, errors.New("Not implemented")
}


有了这个,我们现在可以开始编写测试了。

根据 API 和应用程序需求编写测试

在本节中,我们将根据我们之前提出的要求编写测试。

设置和清理测试数据库


鉴于我们将对数据库运行测试,我们需要确保在运行任何测试之前正确设置数据库,并在所有测试完成后进行清理。我们将在所有其他测试之前执行的 TestMain 函数中执行此操作,如下所示。我们假设 a 变量引用了主应用程序:

func TestMain(m *testing.M) {
    a.Initialize(
        os.Getenv("APP_DB_USERNAME"),
        os.Getenv("APP_DB_PASSWORD"),
        os.Getenv("APP_DB_NAME"))

    ensureTableExists()
    code := m.Run()
    clearTable()
    os.Exit(code)
}

我们定义了一个全局变量 a 来代表我们要测试的应用程序。

初始化应用程序后,我们使用 ensureTableExists 函数来确保我们需要测试的表可用。这个函数可以定义如下。该功能需要导入日志模块:

func ensureTableExists() {
    if _, err := a.DB.Exec(tableCreationQuery); err != nil {
        log.Fatal(err)
    }
}


tableCreationQuery 是一个常量,定义如下:

const tableCreationQuery = `CREATE TABLE IF NOT EXISTS products
(
    id SERIAL,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL DEFAULT 0.00,
    CONSTRAINT products_pkey PRIMARY KEY (id)
)`


所有的测试都是通过调用 m.Run() 来执行的,然后我们调用 clearTable() 来清理数据库。这个函数可以定义如下:

func clearTable() {
    a.DB.Exec("DELETE FROM products")
    a.DB.Exec("ALTER SEQUENCE products_id_seq RESTART WITH 1")
}


在这个阶段, main_test.go 应该包含以下内容。请注意,您需要在此文件中引用您的模块名称,因此请根据需要替换最后一个导入。

// main_test.go
package main_test
 
import (
     "os"
     "testing"
     "log"
     "net/http"
     "net/http/httptest"
     "strconv"
     "encoding/json"
     "bytes"
     "github.com/<github username>/<project name>"
 )

var a main.App

func TestMain(m *testing.M) {
    a.Initialize(
        os.Getenv("APP_DB_USERNAME"),
        os.Getenv("APP_DB_PASSWORD"),
        os.Getenv("APP_DB_NAME"))

    ensureTableExists()
    code := m.Run()
    clearTable()
    os.Exit(code)
}

func ensureTableExists() {
    if _, err := a.DB.Exec(tableCreationQuery); err != nil {
        log.Fatal(err)
    }
}

func clearTable() {
    a.DB.Exec("DELETE FROM products")
    a.DB.Exec("ALTER SEQUENCE products_id_seq RESTART WITH 1")
}

const tableCreationQuery = `CREATE TABLE IF NOT EXISTS products
(
    id SERIAL,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL DEFAULT 0.00,
    CONSTRAINT products_pkey PRIMARY KEY (id)
)`

 

为了运行测试,我们需要在 app.go 中实现 App 的 Initialize 方法,与数据库建立连接并初始化路由器。

将 app.go 中的空 Initialize 函数替换为以下代码:

func (a *App) Initialize(user, password, dbname string) {
    connectionString :=
        fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, password, dbname)

    var err error
    a.DB, err = sql.Open("postgres", connectionString)
    if err != nil {
        log.Fatal(err)
    }

    a.Router = mux.NewRouter()  
}

 

注意:除非您的编辑器/IDE 设置为自动导入所需的依赖项,否则您必须手动将 fmt 和日志包添加到导入列表中。

 

当前的 app.go 应该如下所示:

// app.go

package main

import (
    "database/sql"
    "fmt"
    "log"

    "github.com/gorilla/mux"
    _ "github.com/lib/pq"
)

type App struct {
    Router *mux.Router
    DB     *sql.DB
}

func (a *App) Initialize(user, password, dbname string) {
    connectionString :=
        fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, password, dbname)

    var err error
    a.DB, err = sql.Open("postgres", connectionString)
    if err != nil {
        log.Fatal(err)
    }

    a.Router = mux.NewRouter()
}

func (a *App) Run(addr string) { }

 

在这个阶段,虽然我们没有任何测试,但我们应该能够在我们的应用程序上运行 go test 而不会遇到任何运行时错误。

在第一次运行测试之前,请确保您有一个正在运行的 PostgreSQL 实例。启动测试数据库实例的最简单方法是使用 Docker:

docker run -it -p 5432:5432 -d postgres


在您的项目目录中,执行以下命令:

go test -v


注意:如前所述,我们假设数据库的访问详细信息是在上述环境变量中设置的。

执行此命令应导致如下所示:

testing: warning: no tests to run
PASS
ok      github.com/tomfern/go-mux       0.012s


为 API 编写测试


让我们从使用空表测试对 /products 端点的响应开始。该测试可以如下实现。我们必须添加 net/http 模块才能使其工作:

func TestEmptyTable(t *testing.T) {
    clearTable()

    req, _ := http.NewRequest("GET", "/products", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    if body := response.Body.String(); body != "[]" {
        t.Errorf("Expected an empty array. Got %s", body)
    }
}

 

此测试从 products 表中删除所有记录,并向 /products 端点发送 GET 请求。我们使用 executeRequest 函数来执行请求。然后我们使用 checkResponseCode 函数来测试 HTTP 响应代码是否符合我们的预期。最后,我们检查响应的正文并测试它是否是空数组的文本表示。

executeRequest 函数可以如下实现。这个需要 net/httptest 模块:

func executeRequest(req *http.Request) *httptest.ResponseRecorder {
    rr := httptest.NewRecorder()
    a.Router.ServeHTTP(rr, req)

    return rr
}

 

此函数使用应用程序的路由器执行请求并返回响应。

checkResponseCode 函数可以实现如下:

func checkResponseCode(t *testing.T, expected, actual int) {
    if expected != actual {
        t.Errorf("Expected response code %d. Got %d\n", expected, actual)
    }
}


如果您现在再次运行测试,您应该会得到如下内容:

$ go test -v 

=== RUN   TestEmptyTable
--- FAIL: TestEmptyTable (0.01s)
    main_test.go:73: Expected response code 200. Got 404
    main_test.go:58: Expected an empty array. Got 404 page not found
FAIL
exit status 1
FAIL    github.com/tomfern/go-mux       0.015s


正如预期的那样,测试失败了,因为我们还没有实现任何东西。

我们可以用与上述测试类似的方式来实现其余的测试。

1.获取一个不存在的产品

获取不存在的产品时检查响应的测试可以实现如下。此功能需要 encoding/json 模块:

func TestGetNonExistentProduct(t *testing.T) {
    clearTable()

    req, _ := http.NewRequest("GET", "/product/11", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusNotFound, response.Code)

    var m map[string]string
    json.Unmarshal(response.Body.Bytes(), &m)
    if m["error"] != "Product not found" {
        t.Errorf("Expected the 'error' key of the response to be set to 'Product not found'. Got '%s'", m["error"])
    }
}

 

此测试尝试在端点访问不存在的产品并测试两件事:

  • 状态码为 404,表示未找到该产品,并且
  • 响应包含错误消息“未找到产品”。

2. 创建产品

创建产品的测试可以如下实现。我们需要它的字节模块:

func TestCreateProduct(t *testing.T) {

    clearTable()

    var jsonStr = []byte(`{"name":"test product", "price": 11.22}`)
    req, _ := http.NewRequest("POST", "/product", bytes.NewBuffer(jsonStr))
    req.Header.Set("Content-Type", "application/json")

    response := executeRequest(req)
    checkResponseCode(t, http.StatusCreated, response.Code)

    var m map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &m)

    if m["name"] != "test product" {
        t.Errorf("Expected product name to be 'test product'. Got '%v'", m["name"])
    }

    if m["price"] != 11.22 {
        t.Errorf("Expected product price to be '11.22'. Got '%v'", m["price"])
    }

    // the id is compared to 1.0 because JSON unmarshaling converts numbers to
    // floats, when the target is a map[string]interface{}
    if m["id"] != 1.0 {
        t.Errorf("Expected product ID to be '1'. Got '%v'", m["id"])
    }
}

 

在此测试中,我们手动将产品添加到数据库中,然后访问相关端点以获取该产品。然后我们测试以下内容:

  • HTTP 响应的状态码为 201,表示资源已创建,并且
  • 响应包含一个 JSON 对象,其内容与有效负载的内容相同。

3.获取产品

获取产品的测试可以实现如下:

func TestGetProduct(t *testing.T) {
    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)
}

 

此测试只是将产品添加到表中,并测试访问相关端点会导致 HTTP 响应表示成功,状态码为 200。

在这个测试中,我们使用了 addProducts 函数,该函数用于将一条或多条记录添加到表中进行测试。该功能可以如下实现。它需要 strconv 模块:

func addProducts(count int) {
    if count < 1 {
        count = 1
    }

    for i := 0; i < count; i++ {
        a.DB.Exec("INSERT INTO products(name, price) VALUES($1, $2)", "Product "+strconv.Itoa(i), (i+1.0)*10)
    }
}


4. 更新产品

更新产品的测试可以实现如下:

func TestUpdateProduct(t *testing.T) {

    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)
    var originalProduct map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &originalProduct)

    var jsonStr = []byte(`{"name":"test product - updated name", "price": 11.22}`)
    req, _ = http.NewRequest("PUT", "/product/1", bytes.NewBuffer(jsonStr))
    req.Header.Set("Content-Type", "application/json")

    response = executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    var m map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &m)

    if m["id"] != originalProduct["id"] {
        t.Errorf("Expected the id to remain the same (%v). Got %v", originalProduct["id"], m["id"])
    }

    if m["name"] == originalProduct["name"] {
        t.Errorf("Expected the name to change from '%v' to '%v'. Got '%v'", originalProduct["name"], m["name"], m["name"])
    }

    if m["price"] == originalProduct["price"] {
        t.Errorf("Expected the price to change from '%v' to '%v'. Got '%v'", originalProduct["price"], m["price"], m["price"])
    }
}

 

该测试首先将产品直接添加到数据库中。然后它使用端点用新的细节更新这个记录。我们最终测试了以下内容:

  • 状态码为200,表示成功,
  • 响应包含具有更新详细信息的产品的 JSON 表示。

5. 删除产品

删除产品的测试可以实现如下:

func TestDeleteProduct(t *testing.T) {
    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)
    checkResponseCode(t, http.StatusOK, response.Code)

    req, _ = http.NewRequest("DELETE", "/product/1", nil)
    response = executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    req, _ = http.NewRequest("GET", "/product/1", nil)
    response = executeRequest(req)
    checkResponseCode(t, http.StatusNotFound, response.Code)
}

在这个测试中,我们首先创建一个产品并测试它是否存在。 然后我们使用端点删除产品。 最后,我们尝试在适当的端点访问产品并测试它不存在。

此时,main_test.go 应该如下所示:

// main_test.go

package main

import (
    "os"
    "testing"   
    "log"

    "net/http"
    "net/http/httptest"
    "bytes"
    "encoding/json"
    "strconv"
)

var a App

func TestMain(m *testing.M) {
    a.Initialize(
        os.Getenv("APP_DB_USERNAME"),
        os.Getenv("APP_DB_PASSWORD"),
        os.Getenv("APP_DB_NAME"))

    ensureTableExists()
    code := m.Run()
    clearTable()
    os.Exit(code)
}

func ensureTableExists() {
    if _, err := a.DB.Exec(tableCreationQuery); err != nil {
        log.Fatal(err)
    }
}

func clearTable() {
    a.DB.Exec("DELETE FROM products")
    a.DB.Exec("ALTER SEQUENCE products_id_seq RESTART WITH 1")
}

const tableCreationQuery = `CREATE TABLE IF NOT EXISTS products
(
    id SERIAL,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL DEFAULT 0.00,
    CONSTRAINT products_pkey PRIMARY KEY (id)
)`

func TestEmptyTable(t *testing.T) {
    clearTable()

    req, _ := http.NewRequest("GET", "/products", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    if body := response.Body.String(); body != "[]" {
        t.Errorf("Expected an empty array. Got %s", body)
    }
}

func executeRequest(req *http.Request) *httptest.ResponseRecorder {
    rr := httptest.NewRecorder()
    a.Router.ServeHTTP(rr, req)

    return rr
}

func checkResponseCode(t *testing.T, expected, actual int) {
    if expected != actual {
        t.Errorf("Expected response code %d. Got %d\n", expected, actual)
    }
}

func TestGetNonExistentProduct(t *testing.T) {
    clearTable()

    req, _ := http.NewRequest("GET", "/product/11", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusNotFound, response.Code)

    var m map[string]string
    json.Unmarshal(response.Body.Bytes(), &m)
    if m["error"] != "Product not found" {
        t.Errorf("Expected the 'error' key of the response to be set to 'Product not found'. Got '%s'", m["error"])
    }
}

func TestCreateProduct(t *testing.T) {

    clearTable()

    var jsonStr = []byte(`{"name":"test product", "price": 11.22}`)
    req, _ := http.NewRequest("POST", "/product", bytes.NewBuffer(jsonStr))
    req.Header.Set("Content-Type", "application/json")

    response := executeRequest(req)
    checkResponseCode(t, http.StatusCreated, response.Code)

    var m map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &m)

    if m["name"] != "test product" {
        t.Errorf("Expected product name to be 'test product'. Got '%v'", m["name"])
    }

    if m["price"] != 11.22 {
        t.Errorf("Expected product price to be '11.22'. Got '%v'", m["price"])
    }

    // the id is compared to 1.0 because JSON unmarshaling converts numbers to
    // floats, when the target is a map[string]interface{}
    if m["id"] != 1.0 {
        t.Errorf("Expected product ID to be '1'. Got '%v'", m["id"])
    }
}

func TestGetProduct(t *testing.T) {
    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)
}

// main_test.go

func addProducts(count int) {
    if count < 1 {
        count = 1
    }

    for i := 0; i < count; i++ {
        a.DB.Exec("INSERT INTO products(name, price) VALUES($1, $2)", "Product "+strconv.Itoa(i), (i+1.0)*10)
    }
}

func TestUpdateProduct(t *testing.T) {

    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)
    var originalProduct map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &originalProduct)

    var jsonStr = []byte(`{"name":"test product - updated name", "price": 11.22}`)
    req, _ = http.NewRequest("PUT", "/product/1", bytes.NewBuffer(jsonStr))
    req.Header.Set("Content-Type", "application/json")

    response = executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    var m map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &m)

    if m["id"] != originalProduct["id"] {
        t.Errorf("Expected the id to remain the same (%v). Got %v", originalProduct["id"], m["id"])
    }

    if m["name"] == originalProduct["name"] {
        t.Errorf("Expected the name to change from '%v' to '%v'. Got '%v'", originalProduct["name"], m["name"], m["name"])
    }

    if m["price"] == originalProduct["price"] {
        t.Errorf("Expected the price to change from '%v' to '%v'. Got '%v'", originalProduct["price"], m["price"], m["price"])
    }
}

func TestDeleteProduct(t *testing.T) {
    clearTable()
    addProducts(1)

    req, _ := http.NewRequest("GET", "/product/1", nil)
    response := executeRequest(req)
    checkResponseCode(t, http.StatusOK, response.Code)

    req, _ = http.NewRequest("DELETE", "/product/1", nil)
    response = executeRequest(req)

    checkResponseCode(t, http.StatusOK, response.Code)

    req, _ = http.NewRequest("GET", "/product/1", nil)
    response = executeRequest(req)
    checkResponseCode(t, http.StatusNotFound, response.Code)
}

如果您现在在项目目录中运行 go test -v,您应该会得到类似于以下内容的响应:

$ go test -v

=== RUN   TestEmptyTable
--- FAIL: TestEmptyTable (0.01s)
    main_test.go:75: Expected response code 200. Got 404
    main_test.go:60: Expected an empty array. Got 404 page not found
=== RUN   TestGetNonExistentProduct
--- FAIL: TestGetNonExistentProduct (0.00s)
    main_test.go:91: Expected the 'error' key of the response to be set to 'Product not found'. Got ''
=== RUN   TestCreateProduct
--- FAIL: TestCreateProduct (0.00s)
    main_test.go:75: Expected response code 201. Got 404
    main_test.go:111: Expected product name to be 'test product'. Got '<nil>'
    main_test.go:115: Expected product price to be '11.22'. Got '<nil>'
    main_test.go:121: Expected product ID to be '1'. Got '<nil>'
=== RUN   TestGetProduct
--- FAIL: TestGetProduct (0.01s)
    main_test.go:75: Expected response code 200. Got 404
=== RUN   TestUpdateProduct
--- FAIL: TestUpdateProduct (0.01s)
    main_test.go:75: Expected response code 200. Got 404
    main_test.go:175: Expected the name to change from '<nil>' to '<nil>'. Got '<nil>'
    main_test.go:179: Expected the price to change from '<nil>' to '<nil>'. Got '<nil>'
=== RUN   TestDeleteProduct
--- FAIL: TestDeleteProduct (0.01s)
    main_test.go:75: Expected response code 200. Got 404
    main_test.go:75: Expected response code 200. Got 404
FAIL
exit status 1
FAIL    github.com/tomfern/go-mux       0.066s

 

在这个阶段,我们所有的测试都失败了,因为我们还没有实现任何东西。但是,现在我们的测试已经到位,我们可以开始在我们的应用程序中实现所需的功能。

添加应用程序功能


在本节中,我们将完成我们的应用程序以满足规范和测试。

实现数据库查询


我们将从在产品上实现这些方法开始。实现相对简单,只包括发出查询和返回结果。这些方法可以在model.go中实现如下:

func (p *product) getProduct(db *sql.DB) error {
    return db.QueryRow("SELECT name, price FROM products WHERE id=$1",
        p.ID).Scan(&p.Name, &p.Price)
}

func (p *product) updateProduct(db *sql.DB) error {
    _, err :=
        db.Exec("UPDATE products SET name=$1, price=$2 WHERE id=$3",
            p.Name, p.Price, p.ID)

    return err
}

func (p *product) deleteProduct(db *sql.DB) error {
    _, err := db.Exec("DELETE FROM products WHERE id=$1", p.ID)

    return err
}

func (p *product) createProduct(db *sql.DB) error {
    err := db.QueryRow(
        "INSERT INTO products(name, price) VALUES($1, $2) RETURNING id",
        p.Name, p.Price).Scan(&p.ID)

    if err != nil {
        return err
    }

    return nil
}

让我们也实现 getProducts 函数,如下所示:

func getProducts(db *sql.DB, start, count int) ([]product, error) {
    rows, err := db.Query(
        "SELECT id, name,  price FROM products LIMIT $1 OFFSET $2",
        count, start)

    if err != nil {
        return nil, err
    }

    defer rows.Close()

    products := []product{}

    for rows.Next() {
        var p product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }

    return products, nil
}

 

此函数从 products 表中获取记录。它根据 count 参数限制记录数。 start 参数确定在开始时跳过多少条记录。如果您有很多记录并想要翻阅它们,这会派上用场。

注意:除非您的编辑器/IDE 设置为管理依赖项,否则您必须手动从 model.go 的导入列表中删除错误包。

编辑完成后,您应该会找到 model.go,如下所示:

// model.go

package main

import (
    "database/sql"
)

type product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func (p *product) getProduct(db *sql.DB) error {
    return db.QueryRow("SELECT name, price FROM products WHERE id=$1",
        p.ID).Scan(&p.Name, &p.Price)
}

func (p *product) updateProduct(db *sql.DB) error {
    _, err :=
        db.Exec("UPDATE products SET name=$1, price=$2 WHERE id=$3",
            p.Name, p.Price, p.ID)

    return err
}

func (p *product) deleteProduct(db *sql.DB) error {
    _, err := db.Exec("DELETE FROM products WHERE id=$1", p.ID)

    return err
}

func (p *product) createProduct(db *sql.DB) error {
    err := db.QueryRow(
        "INSERT INTO products(name, price) VALUES($1, $2) RETURNING id",
        p.Name, p.Price).Scan(&p.ID)

    if err != nil {
        return err
    }

    return nil
}

func getProducts(db *sql.DB, start, count int) ([]product, error) {
    rows, err := db.Query(
        "SELECT id, name,  price FROM products LIMIT $1 OFFSET $2",
        count, start)

    if err != nil {
        return nil, err
    }

    defer rows.Close()

    products := []product{}

    for rows.Next() {
        var p product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }

    return products, nil
}

创建路由和路由处理程序


让我们首先为获取单个产品的路由创建处理程序 getProduct。这个处理程序可以在 app.go 中实现如下:

func (a *App) getProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid product ID")
        return
    }

    p := product{ID: id}
    if err := p.getProduct(a.DB); err != nil {
        switch err {
        case sql.ErrNoRows:
            respondWithError(w, http.StatusNotFound, "Product not found")
        default:
            respondWithError(w, http.StatusInternalServerError, err.Error())
        }
        return
    }

    respondWithJSON(w, http.StatusOK, p)
}


您需要将 net/http 和 strconv 模块添加到 app.go。

此处理程序从请求的 URL 中检索要获取的产品的 id,并使用在上一节中创建的 getProduct 方法来获取该产品的详细信息。

如果未找到产品,则处理程序以状态码 404 进行响应,指示无法找到请求的资源。如果找到产品,则处理程序以产品响应。

该方法使用 respondWithError 和 respondWithJSON 函数来处理错误和正常响应。这些功能可以如下实现。它们需要编码/json:

func respondWithError(w http.ResponseWriter, code int, message string) {
    respondWithJSON(w, code, map[string]string{"error": message})
}

func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    response, _ := json.Marshal(payload)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

 

我们可以以类似的方式实现其余的处理程序。

1. 获取产品列表的处理程序

这个处理程序可以在 app.go 中实现如下:

func (a *App) getProducts(w http.ResponseWriter, r *http.Request) {
    count, _ := strconv.Atoi(r.FormValue("count"))
    start, _ := strconv.Atoi(r.FormValue("start"))

    if count > 10 || count < 1 {
        count = 10
    }
    if start < 0 {
        start = 0
    }

    products, err := getProducts(a.DB, start, count)
    if err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, products)
}

此处理程序使用查询字符串中的 count 和 start 参数来获取产品的 count 个数,从数据库中的 start 位置开始。默认情况下,start 设置为 0,count 设置为 10。如果未提供这些参数,此处理程序将响应前 10 个产品。

2. 创建产品的处理程序

该处理程序可以按如下方式实现:

func (a *App) createProduct(w http.ResponseWriter, r *http.Request) {
    var p product
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&p); err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid request payload")
        return
    }
    defer r.Body.Close()

    if err := p.createProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusCreated, p)
}

 

此处理程序假定请求正文是一个 JSON 对象,其中包含要创建的产品的详细信息。它将该对象提取到产品中,并使用 createProduct 方法创建具有这些详细信息的产品。

3. 更新产品的处理程序

该处理程序可以按如下方式实现:

func (a *App) updateProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid product ID")
        return
    }

    var p product
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&p); err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid resquest payload")
        return
    }
    defer r.Body.Close()
    p.ID = id

    if err := p.updateProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, p)
}

与前面的处理程序类似,此处理程序从请求正文中提取产品详细信息。它还从 URL 中提取 id 并使用 id 和 body 来更新数据库中的产品。

4. 删除产品的处理程序

该处理程序可以按如下方式实现:

func (a *App) deleteProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid Product ID")
        return
    }

    p := product{ID: id}
    if err := p.deleteProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"})
}

 

此处理程序从请求的 URL 中提取 id 并使用它从数据库中删除相应的产品。

创建处理程序后,我们现在可以定义将使用它们的路由,如下所示:

func (a *App) initializeRoutes() {
    a.Router.HandleFunc("/products", a.getProducts).Methods("GET")
    a.Router.HandleFunc("/product", a.createProduct).Methods("POST")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.getProduct).Methods("GET")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.updateProduct).Methods("PUT")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.deleteProduct).Methods("DELETE")
}


如您所见,路由是根据我们之前创建的规范定义的。例如,我们使用 a.getProducts 处理程序在 /products 端点处理 GET 请求。

同样,我们使用 a.deleteProduct 处理程序在 /product/{id} 端点处理 DELETE 请求。路径的 {id:[0-9]+} 部分表示 Gorilla Mux 应该仅在 id 是数字时处理 URL。对于所有匹配的请求,Gorilla Mux 然后将实际数值存储在 id 变量中。这可以在处理程序中访问,如上所示,在处理程序中。

现在剩下的就是实现 Run 方法并从 Initialize 方法调用 initializeRoutes。 这可以按如下方式实现:

func (a *App) Initialize(user, password, dbname string) {
    connectionString :=
        fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, password, dbname)

    var err error
    a.DB, err = sql.Open("postgres", connectionString)
    if err != nil {
        log.Fatal(err)
    }

    a.Router = mux.NewRouter()

    a.initializeRoutes()
}

func (a *App) Run(addr string) {
    log.Fatal(http.ListenAndServe(":8010", a.Router))
}

 

app.go 的最终版本应该包含以下代码:

// app.go

package main

import (
    "database/sql"
    "fmt"
    "log"

    "net/http"
    "strconv"
    "encoding/json"

    "github.com/gorilla/mux"
    _ "github.com/lib/pq"
)

type App struct {
    Router *mux.Router
    DB     *sql.DB
}

func (a *App) Initialize(user, password, dbname string) {
    connectionString :=
        fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, password, dbname)

    var err error
    a.DB, err = sql.Open("postgres", connectionString)
    if err != nil {
        log.Fatal(err)
    }

    a.Router = mux.NewRouter()

    a.initializeRoutes()
}

func (a *App) Run(addr string) {
    log.Fatal(http.ListenAndServe(":8010", a.Router))
}

func (a *App) getProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid product ID")
        return
    }

    p := product{ID: id}
    if err := p.getProduct(a.DB); err != nil {
        switch err {
        case sql.ErrNoRows:
            respondWithError(w, http.StatusNotFound, "Product not found")
        default:
            respondWithError(w, http.StatusInternalServerError, err.Error())
        }
        return
    }

    respondWithJSON(w, http.StatusOK, p)
}

func respondWithError(w http.ResponseWriter, code int, message string) {
    respondWithJSON(w, code, map[string]string{"error": message})
}

func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    response, _ := json.Marshal(payload)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

func (a *App) getProducts(w http.ResponseWriter, r *http.Request) {
    count, _ := strconv.Atoi(r.FormValue("count"))
    start, _ := strconv.Atoi(r.FormValue("start"))

    if count > 10 || count < 1 {
        count = 10
    }
    if start < 0 {
        start = 0
    }

    products, err := getProducts(a.DB, start, count)
    if err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, products)
}

func (a *App) createProduct(w http.ResponseWriter, r *http.Request) {
    var p product
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&p); err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid request payload")
        return
    }
    defer r.Body.Close()

    if err := p.createProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusCreated, p)
}

func (a *App) updateProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid product ID")
        return
    }

    var p product
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&p); err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid resquest payload")
        return
    }
    defer r.Body.Close()
    p.ID = id

    if err := p.updateProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, p)
}

func (a *App) deleteProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "Invalid Product ID")
        return
    }

    p := product{ID: id}
    if err := p.deleteProduct(a.DB); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"})
}

func (a *App) initializeRoutes() {
    a.Router.HandleFunc("/products", a.getProducts).Methods("GET")
    a.Router.HandleFunc("/product", a.createProduct).Methods("POST")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.getProduct).Methods("GET")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.updateProduct).Methods("PUT")
    a.Router.HandleFunc("/product/{id:[0-9]+}", a.deleteProduct).Methods("DELETE")
}

运行测试


实现应用程序功能后,我们现在可以再次运行测试:

$ go test -v


这应该会导致所有测试通过,如下所示:

=== RUN   TestEmptyTable
--- PASS: TestEmptyTable (0.01s)
=== RUN   TestGetNonExistentProduct
--- PASS: TestGetNonExistentProduct (0.00s)
=== RUN   TestCreateProduct
--- PASS: TestCreateProduct (0.01s)
=== RUN   TestGetProduct
--- PASS: TestGetProduct (0.01s)
=== RUN   TestUpdateProduct
--- PASS: TestUpdateProduct (0.01s)
=== RUN   TestDeleteProduct
--- PASS: TestDeleteProduct (0.01s)
PASS
ok      github.com/tomfern/go-mux       0.071s


使用信号量(Semaphore)设置持续集成


持续集成 (CI) 是一种加快开发周期的技术。通过建立一个持续测试每个代码更新的短反馈周期,可以在错误出现时立即检测到,团队可以更频繁地安全地合并。

持续集成不需要复杂或昂贵的使用。在本节中,我们将学习如何在几分钟内使用 Semaphore 免费设置它。

将您的存储库添加到 Semaphore
要在存储库中安装 CI/CD 管道,请执行以下步骤:

  • 转到 Semaphore 并使用 Sign up with GitHub 按钮注册一个免费帐户。
  • 单击 + Create new 以将您的存储库添加到 Semaphore。
  • 在列表中找到您的存储库,然后单击选择:

选择 Go starter 工作流程并首先单击自定义它:

当我们选择自定义时,Semaphore 会弹出 Workflow Editor,其中包含以下元素:

  • 管道:管道实现特定目标,例如测试,并组织执行流程。管道由从左到右执行的块组成。
  • 代理:代理是为管道提供动力的虚拟机。我们有三种机器类型可供选择。该机器运行优化的 Ubuntu 18.04 映像,并带有多种语言的构建工具。
  • 块:块是一组可以共享命令和配置的类似作业。块内的作业是并行执行的。一旦一个块中的所有作业都完成了,下一个块就开始了。
  • 作业:作业定义完成工作的命令。他们从父块继承他们的配置。

我们需要对启动器工作流程进行一次修改:

单击测试块。
在右侧,您会找到 Job 命令框。在开头添加以下行:


sem-service start postgres


让我们使用 Go 版本 1.16。将第二行更改为:sem-version go 1.16
并加载测试环境变量。结帐后添加以下行:source env-sample
完整的作业应如下所示:

sem-service start postgres
sem-version go 1.16
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
source env-test
go get ./…
go test ./…
go build -v .

单击运行工作流程,然后单击开始:

就是这样,Semaphore 将立即开始运行管道:

启动一个测试 PostgreSQL 实例。
下载 Go 模块。
运行测试代码。
几秒钟后,我们应该得到测试结果:

改善管道


入门管道在测试代码方面做得很好。但是,这只是一个起点,而不是最终目的地。只需进行一些修改,我们就可以使管道更好地执行和扩展:

  • 缓存模块:现在,每次运行都会重新下载并安装 Go 模块。我们可以通过添加缓存来避免这种情况。
  • 单独的块:我们应该将下载和测试阶段分成两个单独的块。这样,当出现错误时,我们可以更好地确定问题出在哪里。
  • Build:我们可以在管道中编译程序,并保存在工件存储中。
  • 但首先,让我们检查一下 Semaphore 提供的一些内置命令:
  • checkout:checkout 命令会克隆 GitHub 存储库的正确版本并更改目录。它通常是作业中的第一个命令。
  • sem-version:使用 sem-version,我们可以切换一种语言的活动版本。 Semaphore 完全支持多种语言,包括 Go。
  • 缓存:缓存命令提供对信号量缓存的读写访问,这是一个项目范围的作业存储。
  • sem-service:这个工具可以启动多个数据库实例和其他服务。查看管理服务页面以查找受支持的服务。我们可以用一个命令启动一个 PostgreSQL 数据库:
sem-service start postgres 11


所以,让我们让这些命令工作:

单击 Edit Workflow 按钮以再次打开 Workflow Editor:
将块的名称更改为“安装”。
将作业名称更改为“下载模块”。
打开右侧的环境变量部分。创建以下变量。这些变量告诉 Go 将模块存储在本地目录而不是 GOPATH 中。

  • GO111MODULE = on
  • GOFLAGS = -mod=vendor


清除作业命令框的内容并输入:

sem-version go 1.16
checkout
cache restore
go mod vendor
cache store

如您所见,第一个块只负责将模块下载到 vendor/ 目录(go mod vendor)并将它们存储在缓存中。

下一个块运行测试:

单击+添加块虚线按钮以创建一个新块。
将块和作业称为“测试”。
打开环境变量并像以前一样创建 GO111MODULE 和 GOFLAGS 变量。
打开序言并键入以下命令。 序言在块中的每个作业之前执行:


ssem-version go 1.13
sem-service start postgres
checkout
cache restore 
go mod vendor
source env-sample


在命令框中键入以下命令:

go test ./...

最后一个块构建 Go 可执行文件:

添加一个新块。
将块和作业称为“构建”。
重复上一个块中的环境变量和序言步骤。
在框中键入以下命令。 artifact 命令允许我们在项目的工件存储之一中存储和检索文件。

go build -v -o go-mux.bin

artifact push project --force go-mux.bin

单击运行工作流程,然后单击开始。
管道应该在几分钟内完成:

导航到项目的顶层以找到 Project Artifacts 按钮:

您应该在那里找到已编译的二进制文件:


好工作! 现在,您可以对 Semaphore 不断测试您的代码充满信心地处理该项目。

注意:Semaphore 还有一个简洁的测试报告功能,可以让您查看哪些测试失败,找到测试套件中最慢的测试,并查找跳过的测试。 阅读有关该功能以及它如何帮助您的团队的更多信息。

结论


本教程说明了如何使用 Gorilla Mux 和 Postgres 通过 Go 构建 REST API。 我们还了解了如何使用 Semaphore 针对实时 PostgreSQL 数据库持续测试您的应用程序。

如果您有任何问题和意见,请随时将它们留在下面的部分。

文章链接