Intro
As I have already discussed with some members of the Herbs community, I believe that in addition to the use case and the entities, the test scenarios are also part of the domain. This domain knowledge is usually translated from user stories, requirements, etc. to use cases as well as test cases. However, currently Herbs does not support to absorb this knowledge of test scenarios in a structured way.
When we think about how test scenarios are part of the domain, just think use cases are HOW, test scenarios are WHAT. Ex:
WHAT: Allow creating a client in the repository only if it is valid
HOW: Create Client (use case)
Or
WHAT: Do not allow changing a product in the repository if it is expired
HOW: Update Product (use case)
This knowledge is a fundamental part of the domain and should be discussed in a fluid way between developers, product managers, testers, etc. This knowledge should also have greater representation in the code in a structured way, as we have today with entities and use cases.
Given that, I propose a solution to capture this knowledge and expose it via metadata to other glues, especially Shelf.
Aloe - More than a test runner
Ex:
const { CreateProduct } = require('./usecases/createProduct')
spec(CreateProduct, {
'Successful create a simple product': scenario({
description: 'Try to creat a product with just a valid name.',
happyPath: true,
request: {
name: 'A simple product'
},
user: () =>
({ canCreateProduct: true }),
injection: () =>
({ ProductRepo: { save: () => { id: 1 } } }),
'Product must be saved in the repository': check((ret) => {
assert.ok(ret.product.id)
}),
'Product must have a new Id': check((ret) => {
assert.ok(ret.product.id === 1)
}),
}),
'Do not save a product with invalid name': scenario({
description: 'Reject products with invalid names in the repository',
request: {
name: 'A simple product @'
},
user: () =>
({ canCreateProduct: true }),
injection: () => { },
'Product should not be saved in the repository': check((ret) => {
assert.ok(ret.product === null)
assert.ok(ret.isInvalidEntityError)
}),
})
})
The first thing to note is that the spec
is connected to a use case. This is important because current test runners do not have this kind of explicit link with the objects to be tested. Here we want to capture the scenarios of a specific use case and have that as metadata. I see that the natural expansion would be to have spec
not only for use cases but also for entities, but I still haven't thought about how it would be.
Another important point, different from solutions like Cucumber, the code is close to the intent description.
Given / When / Then
When I started thinking about this lib, I was trying to emulate the BDD (behavior driven development) in the spec
structure, but it became clear that some things are already given in the current structure of Herbs and would not need to be rewritten.
The Given is basically the input and is formed by the set of:
injection
for dependency injection.
user
for use case authorization;
- use case
request
and its values;
The When is the execution of the use case, going through the authorization first. But this is transparent and happens automatically.
const uc = usecase(injection)
const hasAccess = await uc.authorize(user)
const response = await uc.run(request)
What we're left with is Then. Here the idea is not only to verify the output of the use case, but also to capture domain knowledge, explaining what is valid and what is not valid for each scenario.
One more example:
spec(ChangeItemPosition, {
'Successful change item position': scenario({
description: 'Try to change the item position in a list with a valid position.',
happyPath: true,
request: {
itemId: 1,
position: 10
},
user: () =>
({ canChangePosition: true }),
injection: () =>
({
ListRepo: { find: (id) => ({ id, items: [] }) },
ItemRepo: { save: () => ({ position: 10 }) }
}),
'Item position must have been changed and saved in the repository': check((ret) => {
assert.ok(ret.item.position === 10)
}),
'Item position must be in a valid range': check((ret) => {
assert.ok(ret.item.position >= 0)
assert.ok(ret.item.position <= 20)
})
}),
'Do not change item position when a position is invalid': scenario({
description: 'A position for a item is invalid when it is out of range',
request: {
itemId: 1,
position: 100
},
user: () =>
({ canChangePosition: true }),
injection: () =>
({
ListRepo: { find: (id) => ({ id, items: [] }) },
ItemRepo: { save: () => ({ position: 10 }) }
}),
'Item position should not be changed in the repository': check((ret) => {
assert.ok(ret.isInvalidPositionError)
}),
})
})
Shelf
Having a structured form of the scenarios is important for the developer to have clarity of the scenarios that that use case is exercised.
But perhaps just as important is that we can extract this knowledge from the code and bring it to the conversation with stakeholders. For that the Shelf would be the ideal tool.
Using the use cases above as an example, it would be possible to build documentation something like:
-
Create Product
- Successful registration with a simple product
- Try to register a product with just a valid name
- Product must be saved in the repository
- Product must have a new Id
- Do not save a product with invalid name
- Reject products with invalid names in the repository
- Product should not be saved in the repository
-
Change Item Position
- Successful change item position
- Try to change the item position in a list with a valid position.
- Item position must have been changed and saved in the repository
- Item position must be in a valid range
- Do not change item position when a position is invalid
- A position for a item is invalid when it is out of range
- Item position should not be changed in the repository
Herbarium
Every spec
should be informed to Herbarium as a new kind of object.
module.exports =
herbarium.specs
.add(CreateProductSpec)
.spec
It would be possible to find specs related to a use case or entity using Herbarium.
Examples / Samples
One functionality that needs to be discussed is the multiple inputs to validate the same scenario. I think of something like this:
spec(CreateProduct, {
'Successful create a simple product': scenario({
...
request: [
{ name: 'A simple product' },
{ name: 'ProdName' },
{ name: 'A simple product' }
],
But it needs to be discussed how each check
has context about which request
item is being executed. Maybe use ctx
instead of just ret (check((ctx)
) and use ctx.ret
and ctx.req
, as in the use case, with ctx.req
containing info about request of that execution.
Spy
Advanced scenarios with spys should be allowed because in some cases it is the only way to validate the output. However, the use of mocks should be discouraged, as they validate and couple to the behavior of the use case and not to the output.
We need to dig deeper to see how these scenarios would look.
Conclusion
This is the beginning of a discussion. Conceptual and implementation insights are welcome. It would be great if someone can bring examples so that we can exercise this model as well.