英文:
How would I test this method?
问题
基本上,我已经开始为Riot Games API编写一个包装器,但我在如何测试它方面遇到了困难。我已经将存储库连接到了Travis,所以在推送时会运行go test
,但我不确定如何进行测试,因为请求所需的API_KEY每天都会更改,我无法自动生成它,如果直接测试端点,我每天都需要手动添加它。
所以我想知道是否可能模拟响应,但在这种情况下,我猜我需要重构我的代码?
所以我创建了一个表示SummonerDTO的结构体:
type Summoner struct {
ID int64 `json:"id"`
AccountID int64 `json:"accountId"`
ProfileIconID int `json:"profileIconId"`
Name string `json:"name"`
Level int `json:"summonerLevel"`
RevisionDate int64 `json:"revisionDate"`
}
该结构体有一个方法:
func (s Summoner) ByName(name string, region string) (summoner *Summoner, err error) {
endpoint := fmt.Sprintf("https://%s.api.riotgames.com/lol/summoner/%s/summoners/by-name/%s", REGIONS[region], VERSION, name)
client := &http.Client{}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, fmt.Errorf("unable to create new client for request: %v", err)
}
req.Header.Set("X-Riot-Token", API_KEY)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to complete request to endpoint: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("request to api failed with: %v", resp.Status)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if err := json.Unmarshal([]byte(respBody), &summoner); err != nil {
return nil, fmt.Errorf("unable to unmarshal response body to summoner struct: %v", err)
}
return summoner, nil
}
这个结构体方法是否没有单一职责,从某种意义上说,它在构建端点、发送请求和解析响应。我需要重构它以使其可测试,如果是这样,最好的方法是什么?我应该创建一个Request和Response结构体,然后对它们进行测试吗?
为了澄清,使用的API密钥受到速率限制,并且需要每天重新生成。Riot Games不允许您使用爬虫自动生成密钥。我正在使用Travis进行持续集成,所以我想知道是否有一种方法可以模拟请求/响应。
可能我的方法是错误的,还在学习中。
希望这些都有一定的意义,如果有疑问,我很乐意进行澄清。
英文:
Essentially I've begun to work on a wrapper for the Riot Games API and I'm struggling with how to test it. I've got the repository plugged into Travis so on push it runs go test
but I'm not sure how to go about testing it since the API_KEY required for the requests changes daily and I can't auto-regenerate it, i'd have to manually add it every day if I tested the endpoints directly.
So I was wondering if it was possible to mock the responses, but in that case I'm guessing I'd need to refactor my code?
So i've made a struct to represent their SummonerDTO
type Summoner struct {
ID int64 `json:"id"`
AccountID int64 `json:"accountId"`
ProfileIconID int `json:"profileIconId"`
Name string `json:"name"`
Level int `json:"summonerLevel"`
RevisionDate int64 `json:"revisionDate"`
}
That struct has a method:
func (s Summoner) ByName(name string, region string) (summoner *Summoner, err error) {
endpoint := fmt.Sprintf("https://%s.api.riotgames.com/lol/summoner/%s/summoners/by-name/%s", REGIONS[region], VERSION, name)
client := &http.Client{}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, fmt.Errorf("unable to create new client for request: %v", err)
}
req.Header.Set("X-Riot-Token", API_KEY)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to complete request to endpoint: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("request to api failed with: %v", resp.Status)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if err := json.Unmarshal([]byte(respBody), &summoner); err != nil {
return nil, fmt.Errorf("unable to unmarshal response body to summoner struct: %v", err)
}
return summoner, nil
}
Is it a case that the struct method doesn't have a single responsibility, in a sense it's building the endpoint, firing off the request and parsing the response. Do I need to refactor it in order to make it testable, and in which case what's the best approach for that? Should I make a Request and Response struct and then test those?
To clarify the API Keys used are rate limited and need to be regenerated daily, Riot Games do not allow you to use a crawler to auto-regenerate your keys. I'm using Travis for continuous integration so I'm wondering if there's a way to mock the request/response.
Potentially my approach is wrong, still learning.
Hopefully that all makes some form of sense, happy to clarify if not.
答案1
得分: 4
编写单元测试包括以下步骤:
- 为所有输入提供已知状态。
- 测试在给定所有可能的输入组合的情况下,是否得到了预期的输出。
因此,首先需要确定输入:
s Summoner
name string
region string
还有任何“隐藏”的输入,例如全局变量:
client := &http.Client{}
输出为:
summoner *Summoner
err error
(如果你编写文件或更改全局变量,还可能有“隐藏”的输出,但在这里似乎没有这样的情况)。
现在,前三个输入很容易为测试创建:只需提供一个空的Summoner{}
(因为你在函数中既不读取也不设置它,所以除了将其设置为空值之外,没有必要设置它)。name
和region
可以简单地设置为字符串。
剩下的部分是http.Client
。至少,你可能应该将其作为参数传递。这不仅使你对测试有控制权,而且还允许你在将来轻松地在生产中使用不同的客户端。
但为了简化测试,你可以考虑实际上传递一个类似客户端的接口,你可以轻松地模拟它。你在client
上调用的唯一方法是Do
,因此你可以轻松地创建一个Doer
接口:
type doer interface {
Do(req *Request) (*Response, error)
}
然后将函数签名更改为接受该接口作为一个参数:
func (s Summoner) ByName(client doer, name string, region string) (summoner *Summoner, err error) {
现在,在你的测试中,你可以创建一个满足doer
接口的自定义类型,并以任何你喜欢的http.Response
进行响应,而无需在测试中使用服务器。
英文:
Writing unit tests consists of:
- Providing known state for all of your inputs.
- Testing that, given all meaning combinations of those inputs, you receive the expected outputs.
So you need to first identify your inputs:
s Summoner
name string
region string
Plus any "hidden" inputs, by way of globals:
client := &http.Client{}
And your outputs are:
summoner *Summoner
err error
(There can also be "hidden" outputs, if you write files, or change global variables, but you don't appear to do that here).
Now the first three inputs are easy to create from scratch for your tests: Just provide an empty Summoner{}
(since you don't read or set that at all in your function, there's no need to set it other than to an empty value). name
and region
can simply be set to strings.
The only part remaining is your http.Client
. At minimum, you should probably pass that in as an argument. Not only does this give you control over your tests, but it allows you to use easily use different client in production in the future.
But to ease testing, you might consider actually passing in a client-like interface, which you can easily mock. The only method you call on client
is Do
, so you could easily create a Doer
interface:
type doer interface {
Do(req *Request) (*Response, error)
}
Then change your function signature to take that as one argument:
func (s Summoner) ByName(client doer, name string, region string) (summoner *Summoner, err error) {
Now, in your test you can create a custom type that fulfills the doer
interface, and responds with any http.Response
you like, without needing to use a server in your tests.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论