Practical Test-Driven Development
本チュートリアルでは、Qmonus SDKでのテスト駆動型開発を学習します。チュートリアルを実践する前にDocs » Scenario » テスト駆動型開発
を読んでおいてください。
Step1. アプリケーションの仕様を決定する
最初にチュートリアルで作成するアプリケーションの仕様を決めます。以下のシーケンスのようにクライアントからの指示でオーダーデータを作成し、外部のHoge
サービス側にもリソースを生成するアプリケーションを作成しましょう。外部のHoge
サービスとのインタラクションは非同期でリソースが生成されるまで状態をポーリングする必要があります。
Hoge
サービスのAPI仕様
外部のHoge
サービスのAPI仕様は、以下のように仮定します。
method | path | request body | response body | success code | error code | description |
---|---|---|---|---|---|---|
POST | /hoges | name, region | hogeID | Accepted | Conflict, InternalError | リソース作成 |
DELETE | /hoges/{hogeID} | hogeID | Accepted | NotFound, InternalError | リソース削除 | |
GET | /hoges/{hogeID} | hogeID, status | Success | NotFound, InternalError | リソース取得 |
Hoge
リソースの状態マシン
Hoge
リソースは以下の状態マシンで駆動すると仮定します。
開発するアプリケーションのAPI仕様
開発するアプリケーションは、1つのAPIです。API仕様は以下の通りとします。
POST /examples
で待ち受け、name
、region
を含んだボディーを元に非同期でHoge
リソースを外部サービスに生成します。応答は、hogeID
です。
method | path | request body | response body | success code | error code |
---|---|---|---|---|---|
POST | /examples | name, region | hogeID | Accepted | BadRequest, Conflict, InternalError |
Step2. テスト観点を実装する
テスト観点は、外部入出力のバリエーションです。これらは、Faker
によって実装することができます。
外部入出力のI/F毎に正常な応答、異常な応答が存在し、それらを網羅的に実装してIllusion
で動作を切り替えることで入出力テストを網羅することができます。
まずは、POST /hoges
のFaker
を定義します。注意点としてHoge
リソースの生成は非同期APIであるため、202応答後、一定時間は、Pending
状態を維持する必要があります。ここでは、Cache
組込みオブジェクトを使用して、15秒間リソースのPending
状態を保持しています。
name: postHoge
category: example
fakes:
Accepted:
script: |-
async def Accepted(*args, **kwargs):
hogeID = uuid.uuid1().hex
await Cache.put(hogeID, "Pending", 15)
return FakeHttpResponse(202, body=dict(hogeID=hogeID))
Conflict:
script: |-
async def Conflict(*args, **kwargs):
return FakeHttpResponse(409)
InternalError:
script: |-
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
次に、アプリケーションが、Active
状態への遷移を監視するための、GET /hoges/{hogeID}
に対してFaker
を定義します。リクエストパスからhogeID
を取り出してPOST時にキャッシュしたPending
状態の存在有無によって返却する状態値を切り替えています。
category: example
name: getHogeWaitForActive
fakes:
InternalError:
script: |-
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
WaitForActive:
script: |-
async def Success(*args, **kwargs):
hogeID = kwargs.get("path").split("/")[-1]
value = await Cache.get(hogeID)
return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active" if value is None else "Processing"))
WaitForError:
script: |-
async def Error(*args, **kwargs):
hogeID = kwargs.get("path").split("/")[-1]
value = await Cache.get(hogeID)
return FakeHttpResponse(body=dict(hogeID=hogeID, status="Error" if value is None else "Processing"))
ここまでの試験観点は、上記のシーケンスに従って、すべてが正常に処理されたときのものです。何か問題が発生した場合(Hoge
リソースの状態がError
に移行した場合を含む)、アプリケーションは、ロールバックしなければなりません。ロールバックは、Hoge
リソースを完全に削除することです。受信したオーダ情報は履歴として残します。
アプリケーションは、ロールバックを開始すると最初にGET /hoges/{hogeID}
を送信してリソースの存在有無をチェックします。この時の応答バリエーションは、存在する
、しない
、わからない
のいづれかです。
category: example
name: getHoge
fakes:
InternalError:
script: |-
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
NotFound:
script: |-
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
Success:
script: |-
async def Success(*args, **kwargs):
hogeID = kwargs.get("path").split("/")[-1]
return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active"))
削除対象が存在する場合、アプリケーションは、DELETE /hoges/{hogeID}
を送信してリソースの削除を試行します。削除も非同期APIであることからPending
状態をキャッシュに15秒間維持します。
category: example
name: deleteHoge
fakes:
Accepted:
script: |-
async def Accepted(*args, **kwargs):
hogeID = kwargs.get("path").split("/")[-1]
await Cache.put(hogeID, "Pending", 15)
return FakeHttpResponse(202, body=dict(hogeID=hogeID))
InternalError:
script: |-
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
NotFound:
script: |-
async def NotFound(*args, **kwargs):
return FakeHttpResponse(404)
最後に、アプリケーションは、リソースが完全に削除されるのをGET /hoges/{hogeID}
を送信して監視します。
category: example
name: getHogeWaitForNotFound
fakes:
InternalError:
script: |-
async def InternalError(*args, **kwargs):
return FakeHttpResponse(500)
WaitForNotFound:
script: |-
async def WaitForNotFound(*args, **kwargs):
hogeID = kwargs.get("path").split("/")[-1]
value = await Cache.get(hogeID)
if value:
return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active"))
else:
return FakeHttpResponse(404)
以上が、Faker
による外部入手力の疑似実装です。
Step3. Illusion
を定義する
Illusion
とは、Step2
で作成したFaker
のfake
動作の集合体です。
全て期待する正常動作となるIllusion
category: example
name: exampleDryrun
fakers:
postHoge: Accepted
getHogeWaitForActive: WaitForActive
POSTが失敗するIllusion
category: example
name: examplePostFailed
fakers:
postHoge: InternalError
Active状態の待機中にエラーが発生し、ロールバックするIllusion
category: example
name: exampleWaitforActiveFailed
fakers:
postHoge: Accepted
getHogeWaitForActive: InternalError
getHoge: Success
deleteHoge: Accepted
getHogeWaitForNotFound: WaitForNotFound
Active状態の待機中にエラーが発生し、ロールバックを試みたが、存在チェックのGETでさらに失敗するIllusion
category: example
name: exampleGetFailed
fakers:
postHoge: Accepted
getHogeWaitForActive: WaitForError
getHoge: InternalError
deleteHoge: Accepted
getHogeWaitForNotFound: WaitForNotFound
Active状態で待機中にエラーが発生し、ロールバックを試みたが、DELETEで失敗するIllusion
category: example
name: exampleDeleteFailed
fakers:
postHoge: Accepted
getHogeWaitForActive: WaitForError
getHoge: Success
deleteHoge: InternalError
getHogeWaitForNotFound: WaitForNotFound
Active状態で待機中にエラーが発生し、ロールバックを試みたが、削除完了の監視ポーリングで失敗するIllusion
category: example
name: exampleWaitforNotFoundFailed
fakers:
postHoge: Accepted
getHogeWaitForActive: WaitForError
getHoge: Success
deleteHoge: Accepted
getHogeWaitForNotFound: InternalError
Active状態の待機中にError状態を検出し、ロールバックするIllusion
category: example
name: exampleDetectedErrorHogeState
fakers:
postHoge: Accepted
getHogeWaitForActive: WaitForError
getHoge: Success
deleteHoge: Accepted
getHogeWaitForNotFound: WaitForNotFound
Step4. テストケースを実装する
いよいよテストケースを作成していきます。Step3でIllusionを定義しましたが、原則としてIllusionとテストケースは1:1の関係になります。
テスト分類 | Illusion | テストケース名 |
---|---|---|
Input validation test | Empty body | exampleValidationBodyEmpty |
Empty headers | exampleValidationHeaderEmpty | |
Bad Content-Type | exampleValidationHeaderContentType | |
Unspecified name |
exampleValidationBodyNameRequired | |
Unspecified region |
exampleValidationBodyRegionRequired | |
Normal test | exampleDryrun | exampleNormalDryrun |
Subnormal test | examplePostFailed | examplePostFailed |
exampleWaitforActiveFailed | exampleSubnormalWaitforActiveFailed | |
exampleDetectedErrorHogeState | exampleSubnormalDryrun | |
Abnormal test | exampleGetFailed | exampleAbnormalGetFailed |
exampleDeleteFailed | exampleAbnormalDeleteFailed | |
exampleWaitforNotFoundFailed | exampleAbnormalWaitforNotFoundFailed |
exampleValidationBodyEmpty
空bodyを受信したら、400
応答することを確認します。
category: example
name: exampleValidationBodyEmpty
target: example
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
return dict()
assertion:
output: |-
async def assertion():
assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationHeaderEmpty
空headerを受信したら、400
応答することを確認します。rstr
モジュールを利用してランダムなbody値を生成すると便利です。
category: example
name: exampleValidationHeaderEmpty
target: example
input:
method: POST
path: /examples
headers: {}
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationHeaderContentType
application/json以外のContent-Typeを受信したら、400
応答することを確認します。
category: example
name: exampleValidationHeaderContentType
target: example
input:
method: POST
path: /examples
headers:
Content-Type: application/xml
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationBodyNameRequired
name
キーが含まれていないbodyを受信したら、400
応答することを確認しています。
category: example
name: exampleValidationBodyNameRequired
target: example
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationBodyRegionRequired
region
キーが含まれていないbodyを受信したら、400
応答することを確認しています。
category: example
name: exampleValidationBodyRegionRequired
target: example
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.word())
assertion:
output: |-
async def assertion():
assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleNormalDryrun
正しい要求であれば、202
応答を返却し、トランザクションがComplete
することを確認しています。
category: example
name: exampleNormalDryrun
target: example
illusion: exampleDryrun
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Complete", "Transaction is in an unexpected state %r" % Transaction.status
examplePostFailed
HogeリソースのPOSTに失敗すると、500
応答を返却することを確認しています。
category: example
name: examplePostFailed
target: example
illusion: examplePostFailed
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==500, "Invalid response code %r" % Response.code
exampleSubnormalWaitforActiveFailed
HogeリソースのPOSTに成功すると、202
応答を返却することを確認しています。その後、Hogeリソースが正常に遷移しなかった場合は、トランザクションがロールバックされていることを確認しています。
category: example
name: exampleSubnormalWaitforActiveFailed
target: example
illusion: exampleWaitforActiveFailed
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
exampleSubnormalDryrun
HogeリソースのPOSTに成功すると、202
応答を返却することを確認しています。その後、HogeリソースがErrorに遷移した場合は、トランザクションがロールバックされていることを確認しています。
category: example
name: exampleSubnormalDryrun
target: example
illusion: exampleDetectedErrorHogeState
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
exampleAbnormalGetFailed
HogeリソースのPOSTに成功すると、202
応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソース情報の取得が失敗し、Aborted
に遷移していることを確認後、強制ロールバックを実行してForceCancelled
に遷移することを確認しています。
category: example
name: exampleAbnormalGetFailed
target: example
illusion: exampleGetFailed
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Aborted", "Transaction is in an unexpected state %r" % Transaction.status
cleanup: |-
async def cleanup():
await Transaction.cancel(force=True)
assert Transaction.status=="ForceCancelled", "Force cancel failed %r" % Transaction.status
exampleAbnormalDeleteFailed
HogeリソースのPOSTに成功すると、202
応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソースの削除が失敗し、Aborted
に遷移していることを確認後、強制ロールバックを実行してForceCancelled
に遷移することを確認しています。
category: example
name: exampleAbnormalDeleteFailed
target: example
illusion: exampleDeleteFailed
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Aborted", "Transaction is in an unexpected state %r" % Transaction.status
cleanup: |-
async def cleanup():
await Transaction.cancel(force=True)
assert Transaction.status=="ForceCancelled", "Force cancel failed %r" % Transaction.status
exampleAbnormalWaitforNotFoundFailed
HogeリソースのPOSTに成功すると、202
応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソースの削除ポーリングが失敗し、Aborted
に遷移していることを確認後、強制ロールバックを実行してForceCancelled
に遷移することを確認しています。
category: example
name: exampleAbnormalWaitforNotFoundFailed
target: example
illusion: exampleWaitforNotFoundFailed
input:
method: POST
path: /examples
headers:
Content-Type: application/json
body: |-
def randomBody():
import rstr
return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
output: |-
async def assertion():
assert Response.code==202, "Invalid response code %r" % Response.code
assert Response.body, "Empty body %r" % Response.body
assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
progress:
- index: 1
script: |-
async def assertion():
assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
assert Transaction.xglobals.hogeID is not None, "hogeID is None"
end: |-
async def assertion():
assert Transaction.status=="Aborted", "Transaction is in an unexpected state %r" % Transaction.status
cleanup: |-
async def cleanup():
await Transaction.cancel(force=True)
assert Transaction.status=="ForceCancelled", "Force cancel failed %r" % Transaction.status
Step5. テストスイートを定義する
テストケースが作成されたので、それらをテストスイートに統合しましょう。テストをまとめて実行するのに便利です。
category: example
name: exampleSuite
suites:
testcases:
- exampleValidationHeaderEmpty
- exampleValidationHeaderContentType
- exampleValidationBodyEmpty
- exampleValidationBodyNameRequired
- exampleValidationBodyRegionRequired
- examplePostFailed
- exampleNormalDryrun
- exampleSubnormalDryrun
- exampleAbnormalGetFailed
- exampleAbnormalDeleteFailed
- exampleAbnormalWaitforNotFoundFailed
- exampleSubnormalWaitforActiveFailed
Step6. アプリケーションを実装する
最後にアプリケーションを作成しましょう。ここまででテスト環境は作成済ですのでFrontalでの開発時にTDDをオンにしてテストを駆動しながら実装するのも良いでしょう。
また、以下のATOMやシナリオは実装例です。これまでに定義したテストをクリアするアプリケーションを自由に作成してみてください。
ATOM
作成するAPIは、要求された情報および注文情報としてデータベースに保持します。ATOMで注文情報のCRUDモデルを作成します。
category: example
name: Example
persistence: true
abstract: false
api_generation: false
attributes:
identifier:
field_immutable: true
field_name: name
field_persistence: true
field_type: string
local_fields:
- field_immutable: false
field_name: region
field_nullable: true
field_persistence: true
field_type: string
field_unique: false
- field_immutable: false
field_name: description
field_nullable: true
field_persistence: true
field_type: string
field_unique: false
- field_immutable: false
field_name: hogeID
field_nullable: true
field_persistence: true
field_type: string
field_unique: false
- field_immutable: false
field_name: createdAt
field_nullable: true
field_persistence: true
field_type: DateTime
field_unique: false
ref_fields: []
methods:
class_methods: []
instance_methods: []
注文情報には識別子としてname
があり、属性としてregion
、description
、createdAt
が定義されています。また、hoge
リソースに割り当てられたhogeID
を保持します。
Scenario
最後に、シナリオを作成します。 TDD自動運転モードを有効にして、シナリオを実装してください。すべてのテストに合格することを願っています!
category: Tutorial
name: example
method: POST
uri: /examples
routing_auto_generation_mode: true
global_variables:
example:
initial: null
hoge:
initial: null
hogeID:
initial: null
name:
description: resource name
initial: null
r:
initial: null
region:
description: region name
initial: null
transaction:
async: true
auto_response: false
enable: true
commands:
- command: request_validation
label: Order Validation
kwargs:
aspect_options:
post:
process: |-
(name, region) = (context.request.body.name, context.request.body.region)
body:
properties:
description:
type: string
name:
pattern: ^[A-Z][a-zA-Z_0-9-]+$
type: string
region:
enum:
- jp1
- jp2
type: string
required:
- name
- region
type: object
headers:
properties:
Content-Type:
enum:
- application/json
type: string
required:
- Content-Type
type: object
- command: script
label: Create Hoge
kwargs:
code: |-
faker("ExampleFaker")
await atom.Example(**context.request.body.dictionary).save()
faker("postHoge")
r = await callout(path="/hoges", method="POST", body=dict(name=name, region=region))
if r.error:
raise Error(r.code, reason="POST failed")
hogeID = MU(json.loads(r.body)).hogeID
context.session.set_status(202)
context.session.finish(dict(hogeID=hogeID))
cancellation:
cancellable: true
actions:
- action_type: script
code: |-
if hogeID:
faker("getHoge")
r = await callout(path="/hoges/{}".format(hogeID))
if r.error and r.code != 404:
raise Error(r.code, reason="Unable to get hoge %r" % hogeID)
if r.code!=404:
faker("deleteHoge")
r = await callout(path="/hoges/{}".format(hogeID), method="DELETE")
if r.error and r.code != 404:
raise Error(r.code, reason="Unable to delete hoge %r" % hogeID)
for i in range(30):
faker("getHogeWaitForNotFound")
r = await callout(path="/hoges/{}".format(hogeID))
if r.error and r.code==404:
faker("ExampleFaker")
example = await atom.Example.load(name)
if example is not None:
faker("ExampleFaker")
await example.destroy()
return
await asyncio.sleep(1)
raise Error(500, reason="Status poll retry over")
- command: script
label: Wait for Active State
kwargs:
code: |-
for i in range(30):
faker("getHogeWaitForActive")
r = await callout(path="/hoges/{}".format(hogeID))
if r.error:
continue
hoge = MU(json.loads(r.body))
if hoge.status == "Active":
return
elif hoge.status == "Error":
raise Error(500, reason="Transited to Error state")
await asyncio.sleep(1)
raise Error(500, reason="Status poll retry over")
- command: script
label: Update Order
kwargs:
code: |-
faker("ExampleFaker")
example = await atom.Example.load(name)
(example.hogeID, example.createdAt) = (hogeID, clock.now())
faker("ExampleFaker")
await example.save()
Tip
上述のサンプルでは、illusionプラグインを全て事前に定義していますが、illusionプラグインを事前作成せずにtestcaseに直接fakerのfakeアクションを紐付けて定義することができます。この場合、対応するillusionが必要に応じて自動生成されます。testcaseに対するillusionとfakerの指定と適用条件は以下の通りです。
1. faker未指定且つillusion未指定: なし
2. faker未指定且つillusion指定あり: illusion採用
3. faker指定あり且つillusion未指定: illusion自動生成
4. faker指定あり且つillusion指定あり:
- 指定illusionとfake動作が一致している: 指定illusionを採用
- 指定illusionとfake動作が一致していない: illusion自動生成
Tip
testcaseの各フェーズで変数を共有したい場合(例えばpreparationでテストデータを生成し、cleanupで削除する際にIDを持ち回りたいなど)、フェーズのテスト関数から共有したい変数をreturnします。returnしたデータは、次以降のフェーズの関数内で Results
という辞書変数で参照できます。キーはフェーズ名となります。preparationフェーズでreturnしたデータは、Results["preparation"]
で参照できます。尚、プラグインモジュールやファンクションが格納される context
変数に埋め込んで共有する方法もありますが、名前競合の観点から推奨しません。 Results
共有変数は、v21.2LTS-patch20210910
以降のversionで利用できます。