This repository shows how we can practice Acceptance Test-Driven Development to develop Shiny apps.
- Start with a vague wish.
- Turn the wish into User Stories.
- Create examples for each User Story.
- Turn scenarios into executable specifications and working software.
Every software project starts with some wish we want to fulfill.
At the beginning it's usually vague, like:
"I wish I could manage my personal finances better."
After we capture the wish, we can start breaking it down into smaller pieces and think about what it actually means.
- "As a user, I want to see my total income and expenses so that I can understand my current financial situation."
- "As a user, I want to categorize my expenses so that I can identify areas where I can save money."
- "As a user, I want to set a monthly budget and track how much I have spent against it."
Once we are ready with our User Stories, we can start creating examples that will fullfil the User Stories.
- Confusing UI and behaviour. Don't write specifications with keywords like "click", "type", "select", "field", etc. If we don't speficy the UI, we can change it without changing the tests. It also allows us to innovate as we don't prescribe how the UI should look.
- Making scenarios too long. Keep them short and focused on one outcome whenever possible.
- No reuse of steps. How can there be a system with no common steps to different things? Think about how you phrase your steps and reuse them in different scenarios.
- Too many scenarios. Don't use only scenarios as automated tests, use them to cover behaviour of users, not every possible edge case.
Example of a scenario for User Story 1.
Given I have recorded my income of $2000
And I have recorded my expenses of $500
When I view my dashboard
Then I should see my total income as $2000
And my total expenses as $500
And my net balance as $1500
Example of a scenario for User Story 2.
Given I have recorded an expense of $50 for "Groceries"
And an expense of $100 for "Entertainment"
When I view my categorized expenses
Then I should see "Groceries" total as $50
And "Entertainment" total as $100
You can see the implementation of tests and the app, but I recommend you go through the commits to see how the app was developed step by step.
- Create
tests/acceptance/
directory for acceptance tests. - Create
.test_acceptance()
command to conveniently run acceptance tests. - Create acceptance test stage on CI.
- Create a test file in
tests/acceptance/
. - Create the first test case, put the scenario name in
test_that()
. - Call functions that will become our domain specific language. The don't exist yet, create an interface that will be easy to read and feed needed parameters.
- Implement domain specific language in a separate file, force verify functions to fail, so that we know our software is not working yet.
- Verify that acceptance tests fail β.
- Create a wrapper for
shinytest::AppDriver
to allow adding our own methods of interacting with the System Under Test (SUT). - Add a method for interacting with the SUT. It's purpose is to hide the implementation details of how we interact with the SUT. If we ever want to interact with the SUT in another way, we would implement another driver with a method with the same name and interface.
- Call the driver method from the DSL function.
- Tests should fail due to a timeout, tests want to interact with an element that doesn't exist yet β.
- Implement the numeric input needed by the first scenario step.
- Run tests to validate that tests pass successfully through
record_income()
step. - Tests should be still failing due to forced fail in verify function β.
- Implement
record_expense()
step. - Run tests to check that tests fail at this step β.
- Implement the numeric input needed by
record_expense()
step. - Run tests to validate that tests pass successfully through
record_expense()
step. - Tests should be still failing due to forced fail in verify function β.
- Don't change the DSL, clicking the buttons is a part of
record_income
andrecord_expense
actions. This is why not mentioning UI in the DSL matters, as it allows us to refactor the UI without changing the tests. - Modify the
record_income
andrecord_expense
actions to include the button clicks. - Implement the buttons in production code.
- Tests should be still failing due to forced fail in verify function β.
- We expect that there will be an element from which we extract the numeric value.
- Tests should be still failing, now due a timeout. Total income element is not yet implemented β.
- Implement the business logic of calculating the total based on given income and expenses.
verify_total_income()
should pass, other assertions should fail β.
- Add business logic for calculating total expenses.
- Tests should pass
verify_total_expenses()
step.verify_net_balance()
should fail β.
- Add business logic to verify net balance.
- All assertions should pass β .
- We can safely refactor the code, we will know if code still works as long as tests are passing.
- Add a second test scenario to test if we can record multiple expenses.
- Reuse existing steps to create a new scenario.
- Implement a teardown function to allow running multiple scenarios.
- Add unit tests for an object that the app will interact with to store data to a persistent storage.
- I want to use a
registry
object to run operations on astorage
object. The storage will be a CSV file, but it could as well be a database connection or a S3 bucket. - This is an implementation detail of the app, it doesn't change the behavior of the app β tests are added to
tests/testthat/
, no changes in acceptance tests are needed.
- Create a specification for an interface that will connect the app with a CSV file.
- Create an implementation objects perviously specified in tests.
- Leave the doors open for other implementations of interfaces so that we could extend the implementation to use a database or a S3 bucket.
- The new code is not used in the app code yet. We will do that in the next commits.
- Use new interface of saving inputs in the app.
- Update the acceptance test so that each test case uses it's own storage, making them independent of each other.
- With the Users' needs satisfied (acceptance tests are passing), we can work on improving the style of the app.
- We can move and style elements on the page any way we want.
- As long as we don't change
data-test
attributes or types of elements, acceptance tests will pass. - If we change the type of component, e.g. numeric input to a dropdown, we only need to change the implementation of interaction with this component.