在单元测试中模拟 context.Done()。

huangapple go评论79阅读模式
英文:

Mock context.Done() in unit test

问题

我有一个HTTP处理程序,在每个请求上设置了一个上下文截止时间:

func submitHandler(stream chan data) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
		defer cancel()
		
		// 读取请求体等操作

		select {
		case stream <- req:
			w.WriteHeader(http.StatusNoContent)
		case <-ctx.Done():
			err := ctx.Err()
			if err == context.DeadlineExceeded {
				w.WriteHeader(http.StatusRequestTimeout)
			}
			log.Printf("context done: %v", err)
		}
	}
}

我可以轻松测试http.StatusNoContent头部,但是我不确定如何测试<-ctx.Done()在select语句中的情况。

在我的测试用例中,我构建了一个模拟的context.Context并将其传递给我的模拟http.Request上的req.WithContext()方法,然而,返回的状态码总是http.StatusNoContent,这让我相信在我的测试中select语句总是进入第一个case。

type mockContext struct{}

func (ctx mockContext) Deadline() (deadline time.Time, ok bool) {
	return deadline, ok
}

func (ctx mockContext) Done() <-chan struct{} {
	ch := make(chan struct{})
    close(ch)
	return ch
}

func (ctx mockContext) Err() error {
	return context.DeadlineExceeded
}

func (ctx mockContext) Value(key interface{}) interface{} {
	return nil
}

func TestHandler(t *testing.T) {
	stream := make(chan data, 1)
	defer close(stream)

	handler := submitHandler(stream)
	req, err := http.NewRequest(http.MethodPost, "/submit", nil)
	if err != nil {
		t.Fatal(err)
	}
	req = req.WithContext(mockContext{})

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusRequestTimeout {
		t.Errorf("expected status code: %d, got: %d", http.StatusRequestTimeout, rec.Code)
	}
}

我该如何模拟上下文截止时间已过的情况?

英文:

I have a HTTP handler that sets a context deadline on each request:

func submitHandler(stream chan data) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
		defer cancel()
		
		// read request body, etc.

		select {
		case stream &lt;- req:
			w.WriteHeader(http.StatusNoContent)
		case &lt;-ctx.Done():
			err := ctx.Err()
			if err == context.DeadlineExceeded {
				w.WriteHeader(http.StatusRequestTimeout)
			}
			log.Printf(&quot;context done: %v&quot;, err)
		}
	}
}

I am easily able to test the http.StatusNoContent header, but I am unsure about how to test the &lt;-ctx.Done() case in the select statement.

In my test case I have built a mock context.Context and passed it to the req.WithContext() method on my mock http.Request, however, the status code returned is always http.StatusNoContent which leads me to believe the select statement is always falling into the first case in my test.

type mockContext struct{}

func (ctx mockContext) Deadline() (deadline time.Time, ok bool) {
	return deadline, ok
}

func (ctx mockContext) Done() &lt;-chan struct{} {
	ch := make(chan struct{})
    close(ch)
	return ch
}

func (ctx mockContext) Err() error {
	return context.DeadlineExceeded
}

func (ctx mockContext) Value(key interface{}) interface{} {
	return nil
}

func TestHandler(t *testing.T) {
	stream := make(chan data, 1)
	defer close(stream)

	handler := submitHandler(stream)
	req, err := http.NewRequest(http.MethodPost, &quot;/submit&quot;, nil)
	if err != nil {
		t.Fatal(err)
	}
	req = req.WithContext(mockContext{})

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusRequestTimeout {
		t.Errorf(&quot;expected status code: %d, got: %d&quot;, http.StatusRequestTimeout, rec.Code)
	}
}

How could I mock the context deadline has exceeded?

答案1

得分: 9

经过多次尝试和错误,我找出了问题所在。我不是试图创建一个模拟的context.Context,而是创建了一个带有过期截止时间的新的context.Context,并立即调用返回的cancelFunc。然后,我将其传递给req.WithContext(),现在它完美地工作了!

func TestHandler(t *testing.T) {
    stream := make(chan data, 1)
    defer close(stream)

    handler := submitHandler(stream)
    req, err := http.NewRequest(http.MethodPost, "/submit", nil)
    if err != nil {
        t.Fatal(err)
    }

    stream <- data{}
    ctx, cancel := context.WithDeadline(req.Context(), time.Now().Add(-7*time.Hour))
    cancel()
    req = req.WithContext(ctx)

    rec := httptest.NewRecorder()
    handler.ServeHTTP(rec, req)

    if rec.Code != http.StatusRequestTimeout {
        t.Errorf("expected status code: %d, got: %d", http.StatusRequestTimeout, rec.Code)
    }
}
英文:

So, after much trial and error I figured out what I was doing wrong. Instead of trying to create a mock context.Context, I created a new one with an expired deadline and immediately called the returned cancelFunc. I then passed this to req.WithContext() and now it works like a charm!

func TestHandler(t *testing.T) {
	stream := make(chan data, 1)
	defer close(stream)

	handler := submitHandler(stream)
	req, err := http.NewRequest(http.MethodPost, &quot;/submit&quot;, nil)
	if err != nil {
		t.Fatal(err)
	}

	stream &lt;- data{}
	ctx, cancel := context.WithDeadline(req.Context(), time.Now().Add(-7*time.Hour))
	cancel()
	req = req.WithContext(ctx)

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusRequestTimeout {
		t.Errorf(&quot;expected status code: %d, got: %d&quot;, http.StatusRequestTimeout, rec.Code)
	}
}

答案2

得分: 1

你的mockContext类型的Done方法永远不会返回done,因为没有任何东西写入通道,所以你的goroutine会一直等待,直到关闭通道,从而触发Done状态。如果你希望它立即报告完成,可以尝试以下代码:

func (ctx mockContext) Done() <-chan struct{} {
    ch := make(chan struct{})
    close(ch)
    return ch
}
英文:

Your mockContext type's Done method will never return done, because nothing ever writes to the channel, so your goroutine sits around forever before it closes the channel, thus triggering the Done state. If you want it to immediately report done, try this:

func (ctx mockContext) Done() &lt;-chan struct{} {
    ch := make(chan struct{})
    close(ch)
    return ch
}

huangapple
  • 本文由 发表于 2017年5月13日 05:55:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/43946993.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定