True, that can very much be the case, haha. So here's some context:
I'm building a testing framework to load test an api server. All endpoints are RESTful, and my strategy is to build a bunch of Gatling endpoints that much up to the server's endpoints. Then I can create scenarios using Gatling that mirror real-world use cases. I decided to break each endpoint into 3 parts: Dependencies on other endpoints, OAuth, and the actual request. Most requests (but not all) require OAuth. These are my main concerns:
- Easily modify the guts of the request while minimizing code copy-n-paste. This includes specifying the action (get, patch, update etc) and the types of params (body, url, form, etc)
- Easily specify Dependencies a request relies on. A Dependency is another endpoint that in one way or another sets session variable that this request uses.
- Easily execute OAuth
- For any endpoint, be able to omit Dependency or OAuth execution.
Here's my current structure:
abstract class Request extends Requestable represents a Request which contains the meat of the request (url, name, params, etc...). To create Gatling endpoints, I create objects which extend Request. The use case is that I can then simply call exec(Endpoint) to use an endpoint.
Requestable is a trait which has
var request: ChainBuilder
def executable: ChainBuilder
var dependentParams: Set[String]
var paramsSet: Set[String]
The idea behind Requestable is it represents a request to execute. There's an implicit conversion from Requestable --> ChainBuilder via executable.
What is really tripping me up is how I'm resolving dependencies. Currently, the abstract Request class has a case class Dependencies(request: Request) extends Requestable. The input is used to getDependencies(request, session) so that it can execute each. getDependencies simply returns a Seq[Requestable] representing a sequence of endpoints that should be executed before the actual request is executed. Here's the Dependencies case class:
case class Dependencies(req: Request) extends Requestable {
request =
// Anytime an endpoint is added, it should also be placed here. This allows it to be executed as a Dependency.
// Currently, the builder runs into an infinite loop since each endpoint invariably points to itself in dependencies...
foreach(session => DependencyManager.getDependencies(req, session), "dependency") { CreateUser -> CreateUser,
GetUserInformation -> GetUserInformation,
UpdateUserInformation -> UpdateUserInformation
)
}
}
Request has these methods which utilize the case classes. And remember, request is a var in Requestable, which is the ChainBuilder that represents the actual request.
protected final def dependencies: Dependencies = Dependencies(this)
protected final def oauth = OAuth(this)
To give an example of how I then create an endpoint:
object UpdateUserInformation extends Request {
jsonBody = (session: Session) =>
s"""
|"user":${session("user").as[String] + "a"},
|"email":"newEmail${session("user").as[String]}@something.com",
|"pass":"fooPass"
""".stripMargin
paramsSet = Set()
dependentParams = Set("user", "email", "pass", "userId")
addDependency(this)
requestName = "Update User Information"
url = "foo"
request = exec(
checkRequest(
http(requestName)
.patch(session => baseUrl + url + session("userId").as[String])
.header("Authorization", "${accessToken}")
)
).pause(Environments.shortPause)
protected override def checkRequest(request: HttpRequestBuilder): HttpRequestBuilder = {
request.check(
status.is(200),
jsonPath("$.user").saveAs("user"),
jsonPath("$.email").optional.saveAs("email"),
jsonPath("$.pass").optional.saveAs("pass")
)
}
}
in this case, executable is not overridden, and by default is exec(dependencies).exec(oauth).exec(request)
Ok, so that's a lengthy explanation but I'd rather give more context rather than less. I do appreciate your help. My background is mostly object-oriented--Java and C#--so I tend to think very much in that mindset. There may be a simple solution I'm missing. If so, I'd love to know :)