ATDD all the things
A guest post from Andreas Worm (source: https://www.linkedin.com/pulse/atdd-all-things-andreas-worm)
Why would you test the frontend, if you are not confident that the backend is performing correctly?
In this article we utilize Serenity BDD and the screenplay pattern. Due to the added level of abstraction, it
allows for defining "given-when-then" type scripts to be implemented on the backend services, the web interfaces, the frontend and any other interface you can think of.
I have not yet tested this in collaboration with other people but I would imagine this could lead to:
- more shared QA ownership as different specializations work together on the test implementations
- better BDD scripts. It's highly recommended to not include implementation details in your bdd stories. Offending phrases would become obvious due to the implementation clashing with the description
- hopefully more test driven development
Some critique that could come up:
- "that's too much overhead, instead of three amigos, we now have everyone discussing our domain specific language"- Maybe? Maybe that's intended? Collaboratively working to get a ubiquitous language
- "your example does not use cucumber, our BAs won't read code" - good point. You can still collaborate on the stories, just work on the java files directly or generate the stories with a serenity.dry run. Another advantage of cucumber would be the ability to use the same feature file to implement tests in your java flavored backend test runner and in your js flavored frontend test runner. Like running the same tests against spring services and against your react redux store. Hit me up if you have actually done that and let me know how it went!
- "all the tests on all layers? isn't that a lot of effort without much gain" - it's optional. Do only the happy path e2e tests on the rest api and add service and ui layer tests when they are useful as regression tests.
So how do we start?
Let's do an example:
WHEN an unauthorized user provides correct credentials
THEN they are authenticated
The implicit GIVEN is an SUT that has a concept of what users are. We start by writing a JUnit test (I use kotlin here because it's more readable in my opinion)
@Test
fun `when a user provides valid credentials, they are authenticated` {
unauthorizedUser = Actor("Unauthorized User")
unauthorizedUser.attemptsTo(SuccessfullyAuthenticate())
unauthorizedUser.should(seeThat(TheyAreAuthorized(),`is`(true)))
}
SuccessfullyAuthenticate and TheyAreAuthorized are just an empty Performable and Question, thus running the test would result in an error "they are authorized should be 'true' but was 'null'". Goal achieved, we have a failing test now. Now we can make this test fail better. We can think of a minimal way to tell the actor that they are not authorized. Something like a
@Service
class AuthenticationService {
fun isAuthorized(user: User): boolean = false
}
We now need to tell TheyAreAuthorized to use the service. We do that by giving our actor the Ability to do so
@RunWith(SerenityRunner::class)
@SpringBootTest
class AuthenticationIT {
@Rule @JvmField
var springIntegrationMethodRule = SpringIntegrationMethodRule()
@Autowired
lateinit var authenticationService: AuthenticationService
val unauthorizedUser = new Actor("Unauthorized User")
@Before
fun setUp() {
unathorizedUser.can(UseTheAuthentcationService(authenticationService))
}
@Test
fun `when a user has valid credentials, they are authenticated` {
unauthorizedUser.attemptsTo(SuccessfullyAuthenticate())
unauthorizedUser.should(seeThat(TheyAreAuthorized(),`is`(true)))
}
}
and using the Ability in the Question implementation
class TheyAreAuthorized: Question<Boolean> {
@Override
fun answeredBy(actor: Actor): Boolean {
val service = UseTheAuthentcationService.as(actor)
return service.isAuthorized(User(actor.name))
}
}
Running the test a second time should result in error: "they are authorized should be 'true' but was 'false'". Progress! Now to the authentication part. Like in the question, you can use the service in the performable as well.
class SuccessfullyAuthenticate: Performable {
@Override
fun performAs(actor: Actor) {
val service = UseTheAuthentcationService.as(actor)
service.authenticate(User(actor.name), "validCredentials")
}
}
service.authenticate does not exists yet and I won't detail the implementation here. Your implementation might be filling a simple map with user names and their authentication status. It later might involve databases and auth tokens. The high level steps stay the same and when your implementation works
unauthorizedUser.should(seeThat(TheyAreAuthorized(), `is`(true)))
returns green.
Onward to the UI
The same behavior description should hold true even when we now use a browser to access the SUT. Implementing that, we need to assign another ability. Also, we need to make sure our actor knows what layer we want to test. Use parameterized test runners to just iterate through the layers or provide the layer as environment variable.
@Managed
lateinit var aBrowser: WebDriver
@Before
fun setUp() {
unauthorizedUser.can(UseTheAuthentcationService(authenticationService))
unauthorizedUser.can(BroweTheWeb.with(aBrowser))
unauthorizedUser.remember("layer under test", layerUnderTest)
}
Based on the remembered layer, we need to switch between implementations
class SuccessfullyAuthenticate: Performable {
@Override
fun performAs(actor: Actor) {
when (actor.recall("layer under test") as LayerUnderTest) {
SPRING_SERVICES -> UseTheAuthentcationService.as(actor)
.authenticate(User(actor.name), "validCredentials")
REST_API -> TODO()
BROWSER -> {
actor.attemptsTo(
Open.url("localhost:3000"),
Click.on(".login"),
Enter.theValue("validCredentials")
.into(".input-password").thenHit(Keys.Enter)
)
}
}
}
}
We state that the minimal functionality is someone navigating to a site, clicking on "login" and filling an input, then hitting enter. For the question we define that when you are already authenticated, navigating to the login page gives us a success message. Now you just need to implement the page, form and success message on the frontend.
class TheyAreAuthorized: Question<Boolean> {
@Override
fun answeredBy(actor: Actor): Boolean {
return when (actor.recall("layer under test") as LayerUnderTest) {
SPRING_SERVICES -> UseTheAuthentcationService.as(actor)
.isAuthorized(User(actor.name))
REST_API -> TODO()
BROWSER -> {
actor.attemptsTo(
Open.url("localhost:3000/login"),
)
actor.should(seeThat(the(SUCCESS_MESSAGE), isVisible()))
return true
}
}
}
}
That's it folks
This is how you could do ATDD on multiple layers with Serenity BDD without using cucumber. There is no specific example project for this article because it is a more condensed explanation of things I have done with
-
https://github.com/globalworming/serenity-screenplay-spring-layers
-
https://github.com/globalworming/spring-service-testing-serenity-screenplay
so please check out my repositories on github.
Or drop a comment on what you want to have explained in more detail or what issues you see with this approach!