TDD, BDD in Javascript World. Node. Refactoring and notes. 5/5
In Part 4 we implemented the necessary code to satisfy the assertions we made in the acceptance criteria which we stipulated with the customer/PO and PM for that specific feature.
If you are lost with code/files, you can always check the repo to see the complete project
In Part 5 you will learn:
- the importance of decision making in BDD, TDD projects
- Possible problems and how to approach them
- How to approach a refactor
- Execute your integration tests against different environments
There will be obstacles
Another question that the PM can ask you is how granular the features should be. In my opinion, developers have to try to understand the business value or how the features and scenarios are presented. If they are presented in a way that would make the test very complex, you should talk to your PM to split those scenarios/features in smaller cases. Only experience can tell you what is the best approach.
It is a process to adapt to BDD, TDD. The benefits are huge, but there are going to be obstacles in our way. It could be inexperience from both business and dev parts, not understanding how to write features, or something else entirely.
Try to keep it simple and always do what is going to help you sleep at night
It is also a plus to have mentors that help you or the company on their way.
About Refactoring
Rule of thumb about refactoring
Whenever you need to attempt a refactor make sure you are pretty well covered with tests
A rookie mistake is to approach a refactor without tests.
How do you ensure that doing modifications to a component or components does not affect the outcome? With manual testing? Logging a couple of things?
If you are a senior developer, you know that you have to do the right thing. In my opinion, do it with elegance and some philosophy.
I test my code because I feel responsible for what I write
Whenever you ask yourself if you should do a test or not
do what is going to help you sleep at night
With that said let’s see what can we refactor in our code.
Identify patterns
If we take a look at our code, we can see this pattern again and again, repeated:
Also, the tests we designed due to that were like this:
the problem here is that we have to provide an entire payload structure, but we really don’t need to do that if we create an abstraction like this:
setWith('my.path', applyFilters(filters))
setWith :: (String, Array -> Array ) -> Payload -> Payload
setWith
has to return a function that takes a Payload type and returns the same type to comply with pipe
rules
applyFilters :: Array -> Array -> Array
applyFilters
takes an array of filters and returns a function that takes an array of values, process them applying the filters and returns the result array
The important abstraction is setWith
. We could use it as a tool that gets a value and a function, and applies the function to the value.
Another example here:
setWith('event.body', JSON.parse)
The result will be a payload with event.body
parsed for that example
Let’s design a test for setWith
If we had passed an identity
function instead of JSON.parse the implementation to make that test pass would be:
function setWith (path, modifier) {
return payload => payload
}
Notice that the test can trick you sometimes. Passing the identity does not make sure that you have to apply it to pass the test. That is why we passed another function like JSON.parse. Nevertheless, if we pass the identity function it has to work too!
The code to pass the test is:
Powerful abstraction right?
It is now time for you to try to refactor the code with this abstraction. Fix the tests accordingly.
The endpoint should look like this:
Make the test pass and remove filterResponse
function and tests after the integration test passes. As you can see, it is a clean process when you stand on the shoulders of giants.
Making abstractions might not be the best thing
Compare the two lines below.
setWith('data.mappedResponse', applyFilters(config.filters))
filterResponse
applyFilters
has a cleaner signature in my opinion, but it needs setWith as an adapter.setWith
can be reused for a lot of functionsfilterResponse
is simpler but has more magic underneath.
This is something that you should debate with your team. What would you do?
BDD and environments
Our case just contemplates a development environment, but how hard would it be to refactor our BDD tests to be able to run in different environments?
Easy, use environment variables:
steps/generic.js
Easy right? Now we just need to change the script in package and pass HOST=http://localhost:3000
or the domain pointing to production. There are several tools like dotenv
that can help you here. Remember not to abuse this.
Adapting your production code to environment variables or adding branching is an anti-pattern and should be avoided.
You can do the same exercise for our express app. Remember to spawn the child process passing the required environment variable.
Merge from lodash can help with that.
const ps = spawn(server.command, server.args)
{ env: merge(process.env, {HOST_EXTERNAL: 'http://localhost:3010' })
Summary
Making abstractions is something that you should discuss with your teammates. Try to always maintain readability and keep a declarative style.
Identify patterns repeating again and again, but don’t be too eager to make the abstraction too soon.
Refactoring should be made replacing small parts one by one and the code should have a good coverage already. New parts or abstractions should be implemented using TDD if new functions are added in the process to satisfy the refactor.
Using environment variables multiply the value of BDD, since you can execute your tests against different environments.