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

sometimes it’s not even easy when there are no obstacles — Photo by cottonbro

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

there is a K.I.S.S. behind that H.E.A.R.T, or that’s what I T.H.I.N.K. — Photo by Natalie

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.

it’s hard to be handsome, elegant and humble — Photo by cottonbro

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.

giants doing some testing — Photo by Bert

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 functions
  • filterResponse 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

Not to be confused with evironment variabulbs — Photo by Singkham

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.

Javascript developer