REST API Testing with Screenplay:从任务的结果中获取一个值,并与其他任务共享
REST API Testing with Screenplay: Get a value from the result of a task, and share it with other tasks
我正在尝试使用 Serenity Screenplay Pattern 通过 REST API 实现登录步骤。为此,我需要能够获取从一个任务返回的 JWT 令牌,并使用它来验证其他任务。我知道理论上该怎么做。请考虑以下 Groovy 代码。
class ActorInTheSpotlight {
String actor
String token
@Step("{0} is an actor using the system under test")
ActorInTheSpotlight whoIsNamed(String actor) {
this.actor = actor
theActorCalled(actor)
return this
}
@Step("#actor who can authenticate with credentials")
ActorInTheSpotlight whoCanAuthenticateWith(String email, String password) {
theActor().whoCan(Authenticate.withCredentials(email, password))
return this
}
@Step("#actor who can call the admin API")
ActorInTheSpotlight whoCanCallTheAdminApi() {
theActor().whoCan(CallAnApi.at("http://localhost:3000"))
return this
}
@Step("#actor was able to login to API with credentials")
ActorInTheSpotlight wasAbleToLoginToApi() {
return wasAbleTo(LoginWithApi.usingCredentials())
}
ActorInTheSpotlight wasAbleTo(Performable... todos) {
theActor().wasAbleTo(todos)
return this
}
}
class LoginWithApi implements Task {
@Shared
ActorInTheSpotlight theActor
static LoginWithApi usingCredentials() {
return instrumented(LoginWithApi.class);
}
@Step("{0} logs into api using credentials")
<T extends Actor> void performAs(T actor) {
def auth = Authenticate.asPrincipal(actor)
actor.attemptsTo(
// NOTE: PostToApi is an alias for Post, renaming `with` to `withRequest`
// so that Groovy does not attempt to match it to the default `with(Closure closure)`
PostToApi.at("/login").withRequest({ RequestSpecification req ->
req.header("Content-Type", "application/json")
.body([email: auth.email, password: auth.password])
})
)
}
}
class AdminApiStepDefinitions {
@Shared
ActorInTheSpotlight theActor
@Before
void set_the_stage(){
OnStage.setTheStage(new OnlineCast())
}
@Given(/^that "([^"]*)" is an Admin who may call the rest api$/)
void is_an_admin_who_may_call_the_rest_api(String actor) {
theActor.whoIsNamed(actor)
.whoCanCallTheAdminApi()
}
@Given(/^s?he was able to login to the api with the credentials$/)
void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
def email = credentials.get('email')
def password = credentials.get('password')
theActor
.whoCanAuthenticateWith(email, password)
.wasAbleToLoginToApi()
}
}
因此,理论上,我应该能够在任务之间共享 ActorInTheSpotlight
个步骤,将其用于 store/retrieve 我的 JWT 令牌。我还看到我可以像这样获取令牌值:
String token = SerenityRest.lastResponse()
.jsonPath()
.getObject("token", String.class);
问题是我不确定将这段代码放在步骤定义的上下文中的什么位置。我应该将此令牌的检索作为它自己的步骤来实现,还是有办法在 LoginToApi
任务本身中隐藏此实现细节?
感谢您的宝贵时间!
更新
这是 Authenticate
能力 class,这可能是实现此功能的好地方,但与上述相同的时间问题仍然存在。 IE,我将如何更新能力 "mid-flight" 以便它在正确的时间可用以用于其他任务。
class Authenticate implements Ability {
String email
String password
// instantiates the Ability and enables fluent DSL
static Authenticate withCredentials(String email, String password) {
return new Authenticate(email, password)
}
// NOTE: custom exception class not shown
static Authenticate asPrincipal(Actor actor) throws CannotAuthenticateException {
// complain if someone's asking the impossible
if(!actor.abilityTo(Authenticate.class)){
throw new CannotAuthenticateException(actor.getName())
}
return actor.abilityTo(Authenticate.class)
}
Authenticate(String email, String password) {
this.email = email
this.password = password
}
}
更新 2
我能够将其作为自己的步骤来实现,但我真的不喜欢我的实现细节像这样泄漏到步骤定义中。我会排除任何允许我在没有下面显示的 was_able_to_get_a_valid_jwt_token
步骤的情况下实现这个的答案。
注意:仅显示对原始代码的补充
class ActorInTheSpotlight {
@Step("#actor has a valid JWT token")
ActorInTheSpotlight whoHasTheToken(String token) {
this.token = token
theActor().whoCan(AuthenticateApi.withToken(token))
return this
}
}
class AuthenticateApi implements Ability {
String token
static AuthenticateApi withToken(String token) {
return new AuthenticateApi(token)
}
static AuthenticateApi asPrincipal(Actor actor) throws CannotAuthenticateException {
// complain if someone's asking the impossible
if(!actor.abilityTo(AuthenticateApi.class)){
throw new CannotAuthenticateException(actor.getName())
}
return actor.abilityTo(AuthenticateApi.class)
}
static <T extends Actor> void attempt(final T actor, final RequestSpecification req) {
AuthenticateApi auth = null
try {
auth = AuthenticateApi.asPrincipal(actor)
}
catch(CannotAuthenticateException e) {
// swallow error
}
if(auth) {
req.header("Authorization", "Bearer ${auth.token}")
}
}
AuthenticateApi(String token) {
this.token = token
}
}
class AdminApiStepDefinitions {
// This is what I want to get rid of!
@Given(/^s?he was able to get a valid JWT token$/)
void was_able_to_get_a_valid_jwt_token() {
theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath()
.getObject("token", String.class))
}
}
下面是一个使用 JWT 令牌对请求进行身份验证的任务示例:
class ApiGet implements Task {
static ApiGet from(String resource) {
return instrumented(ApiGet.class, resource)
}
String resource
ApiGet(String resource) {
this.resource = resource
}
@Step("{0} attempts to GET #resource")
<T extends Actor> void performAs(T actor) {
actor.attemptsTo(
// NOTE: GetFromApi is an alias for Get, renaming `with` to `withRequest`
// so that Groovy does not attempt to match it to the default `with(Closure closure)`
GetFromApi.at(resource).withRequest({ RequestSpecification req ->
AuthenticateApi.attempt(actor, req)
req.header("Content-Type", "application/json")
})
)
}
}
好吧,它看起来不是很线程安全,但是 none 这确实是,所以...嗯。这是我想出的。
class AdminApiStepDefinitions {
@Given(/^s?he was able to login to the api with the credentials$/)
void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
def email = credentials.get('email')
def password = credentials.get('password')
theActor
.whoCanAuthenticateWith(email, password)
.wasAbleToLoginToApi()
theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath().getString("token"))
}
}
我正在尝试使用 Serenity Screenplay Pattern 通过 REST API 实现登录步骤。为此,我需要能够获取从一个任务返回的 JWT 令牌,并使用它来验证其他任务。我知道理论上该怎么做。请考虑以下 Groovy 代码。
class ActorInTheSpotlight {
String actor
String token
@Step("{0} is an actor using the system under test")
ActorInTheSpotlight whoIsNamed(String actor) {
this.actor = actor
theActorCalled(actor)
return this
}
@Step("#actor who can authenticate with credentials")
ActorInTheSpotlight whoCanAuthenticateWith(String email, String password) {
theActor().whoCan(Authenticate.withCredentials(email, password))
return this
}
@Step("#actor who can call the admin API")
ActorInTheSpotlight whoCanCallTheAdminApi() {
theActor().whoCan(CallAnApi.at("http://localhost:3000"))
return this
}
@Step("#actor was able to login to API with credentials")
ActorInTheSpotlight wasAbleToLoginToApi() {
return wasAbleTo(LoginWithApi.usingCredentials())
}
ActorInTheSpotlight wasAbleTo(Performable... todos) {
theActor().wasAbleTo(todos)
return this
}
}
class LoginWithApi implements Task {
@Shared
ActorInTheSpotlight theActor
static LoginWithApi usingCredentials() {
return instrumented(LoginWithApi.class);
}
@Step("{0} logs into api using credentials")
<T extends Actor> void performAs(T actor) {
def auth = Authenticate.asPrincipal(actor)
actor.attemptsTo(
// NOTE: PostToApi is an alias for Post, renaming `with` to `withRequest`
// so that Groovy does not attempt to match it to the default `with(Closure closure)`
PostToApi.at("/login").withRequest({ RequestSpecification req ->
req.header("Content-Type", "application/json")
.body([email: auth.email, password: auth.password])
})
)
}
}
class AdminApiStepDefinitions {
@Shared
ActorInTheSpotlight theActor
@Before
void set_the_stage(){
OnStage.setTheStage(new OnlineCast())
}
@Given(/^that "([^"]*)" is an Admin who may call the rest api$/)
void is_an_admin_who_may_call_the_rest_api(String actor) {
theActor.whoIsNamed(actor)
.whoCanCallTheAdminApi()
}
@Given(/^s?he was able to login to the api with the credentials$/)
void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
def email = credentials.get('email')
def password = credentials.get('password')
theActor
.whoCanAuthenticateWith(email, password)
.wasAbleToLoginToApi()
}
}
因此,理论上,我应该能够在任务之间共享 ActorInTheSpotlight
个步骤,将其用于 store/retrieve 我的 JWT 令牌。我还看到我可以像这样获取令牌值:
String token = SerenityRest.lastResponse()
.jsonPath()
.getObject("token", String.class);
问题是我不确定将这段代码放在步骤定义的上下文中的什么位置。我应该将此令牌的检索作为它自己的步骤来实现,还是有办法在 LoginToApi
任务本身中隐藏此实现细节?
感谢您的宝贵时间!
更新
这是 Authenticate
能力 class,这可能是实现此功能的好地方,但与上述相同的时间问题仍然存在。 IE,我将如何更新能力 "mid-flight" 以便它在正确的时间可用以用于其他任务。
class Authenticate implements Ability {
String email
String password
// instantiates the Ability and enables fluent DSL
static Authenticate withCredentials(String email, String password) {
return new Authenticate(email, password)
}
// NOTE: custom exception class not shown
static Authenticate asPrincipal(Actor actor) throws CannotAuthenticateException {
// complain if someone's asking the impossible
if(!actor.abilityTo(Authenticate.class)){
throw new CannotAuthenticateException(actor.getName())
}
return actor.abilityTo(Authenticate.class)
}
Authenticate(String email, String password) {
this.email = email
this.password = password
}
}
更新 2
我能够将其作为自己的步骤来实现,但我真的不喜欢我的实现细节像这样泄漏到步骤定义中。我会排除任何允许我在没有下面显示的 was_able_to_get_a_valid_jwt_token
步骤的情况下实现这个的答案。
注意:仅显示对原始代码的补充
class ActorInTheSpotlight {
@Step("#actor has a valid JWT token")
ActorInTheSpotlight whoHasTheToken(String token) {
this.token = token
theActor().whoCan(AuthenticateApi.withToken(token))
return this
}
}
class AuthenticateApi implements Ability {
String token
static AuthenticateApi withToken(String token) {
return new AuthenticateApi(token)
}
static AuthenticateApi asPrincipal(Actor actor) throws CannotAuthenticateException {
// complain if someone's asking the impossible
if(!actor.abilityTo(AuthenticateApi.class)){
throw new CannotAuthenticateException(actor.getName())
}
return actor.abilityTo(AuthenticateApi.class)
}
static <T extends Actor> void attempt(final T actor, final RequestSpecification req) {
AuthenticateApi auth = null
try {
auth = AuthenticateApi.asPrincipal(actor)
}
catch(CannotAuthenticateException e) {
// swallow error
}
if(auth) {
req.header("Authorization", "Bearer ${auth.token}")
}
}
AuthenticateApi(String token) {
this.token = token
}
}
class AdminApiStepDefinitions {
// This is what I want to get rid of!
@Given(/^s?he was able to get a valid JWT token$/)
void was_able_to_get_a_valid_jwt_token() {
theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath()
.getObject("token", String.class))
}
}
下面是一个使用 JWT 令牌对请求进行身份验证的任务示例:
class ApiGet implements Task {
static ApiGet from(String resource) {
return instrumented(ApiGet.class, resource)
}
String resource
ApiGet(String resource) {
this.resource = resource
}
@Step("{0} attempts to GET #resource")
<T extends Actor> void performAs(T actor) {
actor.attemptsTo(
// NOTE: GetFromApi is an alias for Get, renaming `with` to `withRequest`
// so that Groovy does not attempt to match it to the default `with(Closure closure)`
GetFromApi.at(resource).withRequest({ RequestSpecification req ->
AuthenticateApi.attempt(actor, req)
req.header("Content-Type", "application/json")
})
)
}
}
好吧,它看起来不是很线程安全,但是 none 这确实是,所以...嗯。这是我想出的。
class AdminApiStepDefinitions {
@Given(/^s?he was able to login to the api with the credentials$/)
void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
def email = credentials.get('email')
def password = credentials.get('password')
theActor
.whoCanAuthenticateWith(email, password)
.wasAbleToLoginToApi()
theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath().getString("token"))
}
}