你如何为测试而截断对GitHub的调用?

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

how can you stub calls to GitHub for testing?

问题

我需要使用go-github创建一个Pull Request评论,我的代码可以工作,但现在我想为它编写测试(是的,我知道测试应该先写),这样在测试期间就不会实际调用真实的GitHub服务。

我已经阅读了3篇关于golang stubbing和mocking的博客,但是作为一个新手,我有点迷失,尽管有go-github问题讨论。例如,我写了以下函数:

// 这是我的函数
func GetClient(token string, url string) (*github.Client, context.Context, error) {
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(ctx, ts)
	client, err := github.NewEnterpriseClient(url, url, tc)

	if err != nil {
		fmt.Printf("error creating github client: %q", err)
		return nil, nil, err
	}
    return client, ctx, nil
}

我该如何进行stubbing?

同样,我有这个函数:

func GetPRComments(ctx context.Context, client *github.Client) ([]*github.IssueComment, *github.Response, error)  {
	opts := &github.IssueListCommentsOptions{
		ListOptions: github.ListOptions{
			Page:    1,
			PerPage: 30,
		},
	}
	githubPrNumber, err := strconv.Atoi(os.Getenv("GITHUB_PR_NUMBER"))
	if err != nil || githubPrNumber == 0 {
	  panic("error: GITHUB_PR_NUMBER is not numeric or empty")
	}

	// use Issues API for PR comments since GitHub docs say "This may seem counterintuitive... but a...Pull Request is just an Issue with code"
	comments, response, err := client.Issues.ListComments(
		  ctx,
		  os.Getenv("GITHUB_OWNER"),
		  os.Getenv("GITHUB_REPO"),
		  githubPrNumber,
		  opts)
	if err != nil {
		return nil, nil, err
	}
	return comments, response, nil
}

我应该如何进行stubbing?

我的想法是可能先通过创建自己的结构来使用依赖注入,但我不确定如何做,所以目前我有这个:

func TestGetClient(t *testing.T) {
	client, ctx, err := GetClient(os.Getenv("GITHUB_TOKEN"), "https://example.com/api/v3/")
    c, r, err := GetPRComments(ctx, client)
    ...
}
英文:

I need to create a Pull Request comment using go-github, and my code works, but now I'd like to write tests for it (yes, I'm aware that tests should come first), so that I don't actually call the real GitHub service during test.

I've read 3 blogs on golang stubbing and mocking, but, being new to golang, I'm a bit lost, despite this discussion on go-github issues. For example, I wrote the following function:

// this is my function
func GetClient(token string, url string) (*github.Client, context.Context, error) {
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(ctx, ts)
	client, err := github.NewEnterpriseClient(url, url, tc)

	if err != nil {
		fmt.Printf("error creating github client: %q", err)
		return nil, nil, err
	}
    return client, ctx, nil
}

How could I stub that?

Similarly, I have this:

func GetPRComments(ctx context.Context, client *github.Client) ([]*github.IssueComment, *github.Response, error)  {
	opts := &github.IssueListCommentsOptions{
		ListOptions: github.ListOptions{
			Page:    1,
			PerPage: 30,
		},
	}
	githubPrNumber, err := strconv.Atoi(os.Getenv("GITHUB_PR_NUMBER"))
	if err != nil || githubPrNumber == 0 {
	  panic("error: GITHUB_PR_NUMBER is not numeric or empty")
	}

	// use Issues API for PR comments since GitHub docs say "This may seem counterintuitive... but a...Pull Request is just an Issue with code"
	comments, response, err := client.Issues.ListComments(
		  ctx,
		  os.Getenv("GITHUB_OWNER"),
		  os.Getenv("GITHUB_REPO"),
		  githubPrNumber,
		  opts)
	if err != nil {
		return nil, nil, err
	}
	return comments, response, nil
}

How should I stub that?

My thought was to perhaps use dependency injection by creating my own structs first, but I'm not sure how, so currently I have this:

func TestGetClient(t *testing.T) {
	client, ctx, err := GetClient(os.Getenv("GITHUB_TOKEN"), "https://example.com/api/v3/")
    c, r, err := GetPRComments(ctx, client)
    ...
}

答案1

得分: 2

我会从一个接口开始:

type ClientProvider interface {
  GetClient(token string, url string) (*github.Client, context.Context, error)
}

当测试一个需要调用GetClient的单元时,确保依赖于你的ClientProvider接口:

func YourFunctionThatNeedsAClient(clientProvider ClientProvider) error {
  // 构建你的token和url

  // 获取一个github客户端
  client, ctx, err := clientProvider.GetClient(token, url)
  
  // 使用客户端进行操作

  return nil
}

现在在你的测试中,你可以这样构建一个存根:

// 一个模拟/存根客户端提供者,设置client func以模拟行为
type MockClientProvider struct {
  GetClientFunc func(string, string) (*github.Client, context.Context, error)
}

// 这将为编译器建立MockClientProvider可以用作你创建的接口
func (provider *MockClientProvider) GetClient(token string, url string) (*github.Client, context.Context, error) {
  return provider.GetClientFunc(token, url)
}

// 你的单元测试
func TestYourFunctionThatNeedsAClient(t *testing.T) {
  mockGetClientFunc := func(token string, url string) (*github.Client, context.Context, error) {
    // 在这里进行设置
    return nil, nil, nil // 返回比这个更好的结果
  }

  mockClientProvider := &MockClientProvider{GetClientFunc: mockGetClientFunc}

  // 运行你的测试
  err := YourFunctionThatNeedsAClient(mockClientProvider)

  // 断言你的结果
}

这些想法不是我自己的,我借鉴了之前的一些人的想法;Mat Ryer在一段关于“惯用的Go语言”的精彩视频中提出了这个想法(和其他想法)。

如果你想要存根github客户端本身,可以使用类似的方法,如果github.Client是一个结构体,你可以用接口来隐藏它。如果它已经是一个接口,上述方法可以直接使用。

英文:

I would start with an interface:

type ClientProvider interface {
  GetClient(token string, url string) (*github.Client, context.Context, error)
}

When testing a unit that needs to call GetClient make sure you depend on your ClientProvider interface:

func YourFunctionThatNeedsAClient(clientProvider ClientProvider) error {
  // build you token and url

  // get a github client
  client, ctx, err := clientProvider.GetClient(token, url)
  
  // do stuff with the client

  return nil
}

Now in your test, you can construct a stub like this:

// A mock/stub client provider, set the client func in your test to mock the behavior
type MockClientProvider struct {
  GetClientFunc func(string, string) (*github.Client, context.Context, error)
}

// This will establish for the compiler that MockClientProvider can be used as the interface you created
func (provider *MockClientProvider) GetClient(token string, url string) (*github.Client, context.Context, error) {
  return provider.GetClientFunc(token, url)
}

// Your unit test
func TestYourFunctionThatNeedsAClient(t *testing.T) {
  mockGetClientFunc := func(token string, url string) (*github.Client, context.Context, error) {
    // do your setup here
    return nil, nil, nil // return something better than this
  }

  mockClientProvider := &MockClientProvider{GetClientFunc: mockGetClientFunc}

  // Run your test
  err := YourFunctionThatNeedsAClient(mockClientProvider)

  // Assert your result
}

These ideas aren't my own, I borrowed them from those who came before me; Mat Ryer suggested this (and other ideas) in a great video about "idiomatic golang".

If you want to stub the github client itself, a similar approach can be used, if github.Client is a struct, you can shadow it with an interface. If it is already an interface, the above approach works directly.

huangapple
  • 本文由 发表于 2021年9月24日 22:28:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/69316696.html
匿名

发表评论

匿名网友

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

确定