Sock ShopのLocustシナリオ書いてみた

## Sock Shop全体へ負荷をかけるテストシナリオ
```python
import base64
from locust import HttpUser, task
import random
import time
import string

def randomname(n):
        randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
        return ''.join(randlst)

class WebUser(HttpUser):

    @task
    def load(self):

        headers = {'Content-Type': 'application/json'}
        payload = {
            "Username": randomname(10),  # ユーザネーム
            "First name": "tarou",  #ファーストネーム
            "Last name": "kouka",  #ラストネーム
            "Email": "abc@abc",  # メアド
            "Password": "password123"  # パスワード
        }

        payload_address = {
                "number": "1404-1",
                "street": "katakuramachi",
                "city": "hachiouji",
                "postcode": "1920982",
                "country": "japan"
            }
        
        payload_payment = {
                "longNum": "1234567890123456",
                "expires": "12/24",
                "ccv": "123"
            }

        catalogueList = [
                "3395a43e-2d88-40de-b95f-e00e1502085b",
                "510a0d7e-8e83-4193-b483-e27e09ddc34d",
                "808a2de1-1aaa-4c25-a9b9-6612e8f29a38",
                "819e1fbf-8b7e-4f6d-811f-693534916a8b",
                "837ab141-399e-4c1f-9abc-bace40296bac",
                "a0a4f044-b040-410d-8ead-4de0446aec7e",
                "d3588630-ad8e-49df-bbd7-3167f7efb246",
                "zzz4f044-b040-410d-8ead-4de0446aec7e"
        ]

        item_id = random.choice(catalogueList)

        self.client.get("/")
        #self.client.get("/login", headers={"Authorization":"Basic %s" % base64string})
        self.client.post("/register", json=payload, headers=headers)
        self.client.get("/category.html")
        self.client.get("/detail.html?id={}".format(item_id))
        self.client.delete("/cart")
        self.client.post("/cart", json={"id": item_id, "quantity": 1})
        time.sleep(3)
        self.client.post("/addresses", json=payload_address, headers=headers)
        self.client.post("/cards", json=payload_payment, headers=headers)
        self.client.get("/basket.html")
        self.client.post("/orders")
```

## 公式のテストシナリオが使えないT_T
Sock ShopにLocustで負荷試験を行うために公式のLocustシナリオを使用しようとしたところ，以下のようなエラーが出る．
```
  File "/home/c0a20120/.local/lib/python3.10/site-packages/locust/main.py", line 75, in main
    docstring, _user_classes, shape_class = load_locustfile(_locustfile)
  File "/home/c0a20120/.local/lib/python3.10/site-packages/locust/util/load_locustfile.py", line 58, in load_locustfile
    imported = source.load_module()
  File "<frozen importlib._bootstrap_external>", line 548, in _check_name_wrapper
  File "<frozen importlib._bootstrap_external>", line 1063, in load_module
  File "<frozen importlib._bootstrap_external>", line 888, in load_module
  File "<frozen importlib._bootstrap>", line 290, in _load_module_shim
  File "<frozen importlib._bootstrap>", line 719, in _load
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/c0a20120/locust/locustfile.py", line 27, in <module>
    class Web(HttpLocust):
  File "/home/c0a20120/.local/lib/python3.10/site-packages/locust/util/deprecation.py", line 27, in __new__
    raise ImportError(deprecation_message)
ImportError: The HttpLocust class has been renamed to HttpUser in version 1.0. For more info see: https://docs.locust.io/en/latest/changelog.html#changelog-1-0
```

エラーの内容によると，バージョン1.0からHttpLocustクラスがHttpUserという名前に変更されていた．現在，私の環境のLocustのバージョンは2.15.1であったため，クラスを変更して再度実行してみる．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111732/09b1dda2-6267-d264-a0f3-f3fea14d852c.png)
GUIにアクセスできたので，対象のSock Shopのアドレスを入力し，実際に負荷試験を実行してみた．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111732/69aff67a-5e90-db53-a6e2-93db98127fb3.png)
無事にリクエストを送信できた．しかし，failuresの欄に以下のようなエラーが発生...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111732/cd697a74-6e1a-ec01-080f-dc984b8ae981.png)
/orderへのPOSTリクエストが送信できていないT_T．
これは商品を注文する動作なので，カートにモノがない状態でこのURLにアクセスしようとするとこのようなエラーが発生してしまう．
また，
理由として考えられるのは，Locustでは複数のユーザでリクエストを送信しているけど，テストシナリオでは1つのユーザしか使っていないため，試験中にユーザを共有してる可能性がある．

## orderの対策
orderへのPOSTリクエストがきちんと送信されるために，テストシナリオの最初にユーザを登録する．Sock Shopのユーザ登録画面はこれ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111732/f402b223-125f-9f4d-fee8-258e00368685.png)
登録する際にはUsername，First name，Last name，Email，Passwordの5つの要素が必要．
すべて同じ内容で登録しようとしてもエラーが発生してしまうが，Usernameだけ変更すれば別のユーザとして登録することができる．
そのため，locustfile.pyに下記のようなクラスを追加した．

```python
def randomname(n):
        randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
        return ''.join(randlst)
```

```python
headers = {'Content-Type': 'application/json'}
payload = {
    "Username": randomname(10),  # ユーザネーム
    "First name": "tarou",  #ファーストネーム
    "Last name": "kouka",  #ラストネーム
    "Email": "abc@abc",  # メアド
    "Password": "password123"  # パスワード
}
```
randomnameクラスにより，Usernameが毎回異なる文字列となるようにした．また，その他の要素は固定している．

テストシナリオ内でログインしている部分の変更を行う．

変更前
```python
self.client.get("/login", headers={"Authorization":"Basic %s" % base64string})
```
変更後
```python
self.client.post("/register", json=payload, headers=headers)
```
これで，テストシナリオが実行されるたびに，新たなユーザが作成されるようになった．
でも，ユーザを新たに登録して注文を行う際に，住所とカードの情報が必要．
だから，それぞれの情報をPOSTリクエストで入力していく．
```python
payload_address = {
    "number": "1404-1",
    "street": "katakuramachi",
    "city": "hachiouji",
    "postcode": "1920982",
    "country": "japan"
}
        
payload_payment = {
    "longNum": "1234567890123456",
    "expires": "12/24",
    "ccv": "123"
}
```
これらの情報は異なるユーザで同じものを使用しても大丈夫なためこれで固定した．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111732/d0ee86f4-e886-9fa1-3340-9120ae1cdbfd.png)
orderでエラーが起きることなくSock Shopの全体に負荷試験を行うことができた．
ただ，RPSが増加するとカートにモノを入れてから注文するまでの期間が短くorderでエラーが出ちゃう．
```python
self.client.post("/cart", json={"id": item_id, "quantity": 1})
time.sleep(3)
```
このようにカートにモノを入れた後に期間を開けるとエラーが出なくなる．