ทดสอบโค้ด Go ที่อยู่ในรูปแบบ message loop goroutine

วันนี้เจอโค้ด Go ที่อยู่ในรูปแบบ message loop แล้วต้องเขียนเทสเพื่อทดสอบ goroutine เลยได้ท่าแบบนี้ในการเขียนเทสออกมา

โค้ดอยู่ในรูปแบบประมาณนี้

package sample

import (
	"context"
	"fmt"
)

var println = fmt.Println

func loop(ctx context.Context, msgCh chan string) {
	for {
		select {
		case <-ctx.Done():
			return
		case msg := <-msgCh:
			println(msg)
		}
	}
}

จะเห็นว่าตัวฟังก์ชันรับค่า context กับ channel ซึ่งในตัวฟังก์ชันนั้นจะวนลูปเพื่อเช็คว่ามี message ส่งมาทาง channel หรือไม่ ถ้ามีก็ปริ้น message นั้นออกไป

หรือ ถ้า context นั้นถูก cancel/timeout ไปก่อนแล้ว (เรียก method Done() เพื่อรับค่า channel แล้วพยายามดึงค่าออกมาจาก channel นั้นเพื่อเช็คว่า context นั้นถูก cancel/timeout แล้วหรือยัง) ถ้า context cancel/timeout ไปก่อนแล้วก็จบฟังก์ชัน

ส่วนโค้ดที่เทสสำหรับฟังก์ชันนี้ก็จะได้ประมาณนี้

package sample

import (
	"context"
	"fmt"
	"testing"
)

func TestLoop(t *testing.T) {
	t.Cleanup(func() { println = fmt.Println })

	// stub println function
	var msg string
	println = func(a ...any) (n int, err error) {
		msg = a[0].(string)
		return 1, nil
	}

	ctx, cancelFn := context.WithCancel(context.Background())
	msgCh := make(chan string)

	// run loop in separated goroutine
	go loop(ctx, msgCh)

	// send message
	msgCh <- "Hello, World"

	// trigger done context
	cancelFn()

	if msg != "Hello, World" {
		t.Fatalf("expect \"Hello World\" but got %q", msg)
	}
}
  • เรา stub ฟังก์ชัน println ก่อนแล้วเก็บค่า parameter ที่ส่งมาไว้ในตัวแปร msg เพื่อเอาไว้เช็คว่าโดนเรียกหรือไม่
  • จากนั้นก็สร้าง context ใหม่ โดยใช้ WithCancel เพื่อให้ได้ cancelFn กลับมาด้วยเพื่อสั่ง cancel context ได้
  • สร้าง message channel
  • รัน loop แยกในอีก goroutine นึงพร้อม context กับ message channel ไป
  • ทดสอบส่ง message ไปทาง msgCh
  • สั่ง cancelFn() เพื่อ cancel context แล้วทำให้ goroutine จบการทำงาน
  • เช็คว่า msg มีค่าเท่ากับ message ที่ส่งไปหรือไม่

ก็เท่านี้ น่าจะพอเห็น pattern เอาไว้ทดสอบถ้าเจอฟังก์ชันแบบนี้อีก