Pragmatic to a Fault — November 10, 2020

Pragmatic to a Fault

In the last few years, you’ve probably heard these phrases a lot.

Can we focus on the problem in front of us?

We need to be practical here

Let’s try to be pragmatic

We’ve all worked with engineers who care more about applying SOLID principles than delivering working software. As an industry we’ve been reacting to that – and rightly so. But I have also seen this language used to shut down conversations that – though they may have seemed insufficiently pragmatic at the time – turned out to be very important. We push aside conceptual problems to focus on practical problems, but today’s practical problems are often symptoms of the conceptual problems we pushed aside yesterday. Pragmatism can become a shortsighted cycle of conceptual neglect.

Conceptual Problems You Should Care About

Some conceptual problems can be kept in our peripheral vision until we understand them better, but others need to be addressed right away. How do we know which is which? To try and help, here are some of conceptual problems that have caused issues for my team. When we see one of these now, we tackle it right away.

Inconsistent Thinking

If your code reflects different ways of thinking about and solving the same problem, clashing mental models will cause confusion sooner rather than later.

In publishing certain messages for downstream systems to consume, we needed a mechanism to identify which billing period each message related to. Different messages were implemented by different people over a period of several months and when I examined the whole subsystem at the end, it was clear that the logic for finding the billing period had been invented anew by each engineer. Depending on the message, it was sometimes being found by the bill, sometimes by a created-at timestamp and sometimes by a combination of the two. There were 3 different strategies implemented across 5 different code paths.

When I expressed concern, I was encouraged to be pragmatic – after all, the integration was working fine. But we have enough users that errors in thinking quickly manifest as mishandled edge-cases, so I wanted to get out in front of this one. I began converging our various code paths and strategies around the most sensible solution.

Just before I completed the job a Product Manager in another team sounded the alarm – their numbers weren’t adding up because certain messages couldn’t be placed in billing periods and it was costing us money. Because we had begun solving the conceptual problem instead of waiting for a practical problem, we were able to resolve the issue on the same day and avoid serious financial harm for the business.

Self-Defeating Compromises

We make trade-offs to get things to market faster, but there’s no point delivering something faster if the core value remains unrealised.

One of our teams developed a service for centralising ownership of certain account information. Downstream systems had been designed to expect this information with a special key, but so far only one key had been used. Developers of the new upstream system decided to save time by only supporting that one key.

But downstream systems weren’t designed to expect a key arbitrarily – being able to differentiate these records by key was essential to providing a good user experience. As soon as the business added a second key, the upstream system was rendered useless. The compromise was self-defeating, in that it undermined the core value of the work.

Misleading Behaviour

Anything that doesn’t behave the way it leads people to think it will behave is an expensive mistake waiting to happen.

A member of our team named the primary key column for a new fees table transaction_id. As the team already had a ledger table called transactions and a convention of storing off-ledger supplementary data in other tables (with transaction_id as the foreign key), the naming decision misled many engineers into thinking that the new fees table was another example of this pattern. Much time was wasted explaining to people why they were not able to find those transaction_id values in the transactions table. When the misunderstanding began to influence people’s design decisions, we stepped in and changed it.

Lost Information

We all know not to throw out important information. But many engineers conflate what happened with their interpretation of what happened, discarding the former and only storing the latter. I wrote a whole post on this topic called Write Now, Think Later.

The Real Cost of the Panama Papers — August 6, 2019

The Real Cost of the Panama Papers

On April 16th 1953, U.S. President Dwight D. Eisenhower gave his now famous speech The Chance for Peace. It’s a wide-ranging speech, but the part usually recounted – and closest to my heart – is as follows:

Every gun that is made, every warship launched, every rocket fired signifies, in the final sense, a theft from those who hunger and are not fed, those who are cold and are not clothed. This world in arms is not spending money alone. It is spending the sweat of its laborers, the genius of its scientists, the hopes of its children.

The cost of one modern heavy bomber is this: a modern brick school in more than 30 cities. It is two electric power plants, each serving a town of 60,000 population. It is two fine, fully equipped hospitals. It is some fifty miles of concrete pavement. We pay for a single fighter plane with a half million bushels of wheat. We pay for a single destroyer with new homes that could have housed more than 8,000 people.

Most of us already know what opportunity cost is and we’re probably not surprised to learn that heavy bombers are expensive, but something about Eisenhower’s direct comparison reaches out from the past and shakes us.

It’s been years since we first heard about the Panama Papers. The world’s wealthiest people were caught hiding their money from the rest of us. The numbers were unreal –  so large it was hard to place them in our reality. Trillions of dollars passed through Panamanian firm Mossack Fonseca and while there have been some consequences the matter eventually slipped out of our collective consciousness, untethered as it was to our daily reality.

You can probably see where I’m going with this.

Doing the math

Where do we start? We need to know how much money was hidden – so far so good, the Panama Papers have that covered. Then we need to know how much tax would have been collected – that’s super tricky, because the amount of tax that would have been paid on money had it not been hidden depends on too many variables to name. Finally we’ll need to articulate what else that money could have paid for, which varies wildly depending on where you spend it. Most of these complications arise from the global nature of our problem space – Eisenhower had it easy!

Let’s skip ahead a couple of steps using the prior work of Pierre Moscovici, European Commissioner for Economic and Financial Affairs, Taxation and Customs. Moscovici estimated that offshore tax shelters like those revealed in the Panama Papers cost the public around €1,000,000,000,000 annually. That’s one trillion hard-to-really-comprehend Euro every year.

The Global Partnership for Education estimates the cost of “a complete education from pre-primary through upper secondary”  in the developing world at €5,200 (currency conversion correct at time of writing) – note that this is the lifetime cost of an entire education, not the annual cost. India’s newest public hospital – state-of-the-art and their second largest – was recently completed at a cost of €161,668,600. Finally – for my fellow Melbournians – the cost of building a national-average-sized dwelling on a median-priced lot in Melbourne is apparently €364,600, the average number of occupants per dwelling is 2.7 and the number of homeless in Australia is around 105,000.

With these numbers in hand, we’re ready to look at the Panama Papers in a way we might actually remember.

To borrow from Eisenhower…

Every dollar of tax evaded signifies, in the final sense, a theft from those who hunger and are not fed, those who are cold and are not clothed. Our wealthiest people are not hoarding money alone. They are hoarding the sweat of our labourers, the genius of our scientists and the hopes of our children.

The annual cost of our wealthiest people evading taxes is this: A complete education for at least 190 million children in the developing world. It is more than 6 thousand large, fully-equipped hospitals in India. We pay for tax evasion with new homes that could have housed all of Australia’s homeless 70 times over each year.

Engineering Managers and Ants: The Search for Consistency — July 23, 2019

Engineering Managers and Ants: The Search for Consistency

Have you ever seen (or been) an Engineering Manager or Software Architect looking for consistency between teams? Have you ever seen a trail of ants, carrying food back to their colony? I’m going somewhere with this – I promise.

Imagine (or remember) a trail of ants. The terrain is complex and – for an ant – dangerous. The food source is very small. The ants have a terrible vantage point and can’t get a good look at anything. They’re not very bright, even by our standards. But the path they’ve found? It’s almost perfect! It has some twists and turns, but it’s relatively direct. There’s a dangerous obstacle nearby, but the trail gives it a wide berth. How did the ants find that food? How did they figure out a path that balances safety and directness from such a terrible vantage point and with such tiny brains?

How do the ants do it?

The basic mechanism behind this is well understood and very efficient – so much so that we frequently use it to solve problems of our own. Let’s start with a simplified model of ant navigation:

  1. Ants follow the pheromone trails left by ants
  2. If there’s no pheromone trail to follow, ants wander off in a for-our-purposes-random direction

Drop a colony of ants in the middle of a flat plane with no pheromone trails to follow and they’ll spread out in all directions, each ant leaving a new pheromone trail. When an ant finds food it resets and follows its own pheromone trail back to the colony. This trail is now twice as strong as any other, because it has been traversed twice. Other ants encountering this trail will change course and begin to follow it,  strengthening it further until eventually all the ants are using it to ferry food back to the colony.

Now we know how ants find food. It’s an amazing trick – they’re small, stupid (by our standards) and can’t see what’s going on, but with a couple of simple behaviours they can do route-finding in complex environments. It’s a triumph of distributed intelligence – a whole greater than the sum of its parts.

Imitation is the sincerest form of flattery

So I guess we should do that too, right? Teams try all kinds of ideas until one of them finds something that works, then we all converge on that solution and keep doing it (well, until the food runs out). That’s roughly what I’ve seen in many organisations – perhaps you’ve seen it too:

  • “The other tech leads and I talked about it and decided we should all use <Library> from now on”
  • “<Manager> wants all the story walls to be consistent, so we rearranged yours a bit last night”
  • “All the other teams follow <Process>, so we should really be doing it too”

To some extent this emerges naturally – humans are great at learning by imitation – but often convergence on a single solution is driven from the top down. There are plenty of good reasons a leader might nudge people in that direction and you’ve probably heard some of them:

  • “It’s easier to move between codebases if they all use <Pattern/Library>”
  • “We don’t want to be constantly reinventing the wheel”
  • Homogenous teams and codebases simplify resource allocation and reporting (you don’t hear this one spoken plainly very often, but it’s a biggie)

Local optima

So far, so good. But we haven’t explained the ants’ real secret yet! If ants really used our simplified model of navigation, they’d converge on the first solution they found. You’d get a trail that leads all over the place before it eventually happens to collide with something useful. Ants following it would waste unnecessary energy and be exposed to needless risk using a wildly inefficient solution they arrived at almost by accident. They’d be trapped in a local optimum.

As we said earlier, ants generally do better than that. Ant trails are relatively direct, even over challenging terrain. They’re small, stupid and have a terrible vantage point – so how do they overcome these weaknesses and escape local optima when all they know how to do is follow pheromone trails? Amazingly, they do it by being even worse than you thought! As it turns out, ants can’t even follow pheromone trails properly. They regularly get lost and wander off the trail and in so doing, they sometimes happen upon a shorter or safer route than the one they were trying to follow. Ants that move back and forth along this new route will – quite incidentally – end up increasing the strength of the pheromone trail quicker such that it eventually becomes the dominant trail. After enough of this, the ants will end up with something close to the best trail possible – a global optimum (for a suitably narrow definition of the problem). Their inconsistency is a crucial part of their success.

Intellectually we know the risks of converging too quickly on a solution. When we solve problems using particle swarm optimisation or evolutionary algorithms we are careful to keep introducing random variations and thereby avoid getting trapped in local optima. But when you get a few tech leads, architects or engineering managers together we seem to forget all about it. We forget that inconsistency is almost synonymous with innovation – that it is the most effective means by which new ideas can be tried, evaluated and eventually adopted.

Finding balance

There are good reasons to want some amount of consistency, but we regularly take it so far that I suspect there’s more going on. Maybe we want predictability in a stochastic world, control over empowered teams or some other hopeless paradox. Perhaps in our fear of uncertainty and our haste to converge on the safe and the known, we trap ourselves in local optima. Not only does this limit our potential, it is deeply disempowering to the people doing the work. After all, even ants are allowed to wander off the trail!

While a little consistency can be very useful, total consistency is like rigor mortis. No new tools, techniques or ideas. Disempowered developers stuck in the past, feeling like pawns and taking orders from people who think they found all the answers years ago in a rapidly changing world. Developers working under such a regime will – if they have any interest in their field or their sanity – quit and find something more interesting to do.

To find balance,  we need only stop meddling. Humans are big imitation learners, we often go along with bad ideas just to fit in and we’ve all heard that good developers are lazy. If despite all of this your teams are still trying out something new, then they probably have a strong intuition that they are in local optima – that a better solution exists somewhere out there. We should respect that intuition and avoid upsetting the balance with top-down pressure to converge prematurely.

Write Now, Think Later — December 19, 2018

Write Now, Think Later

When building software, we often make key decisions right at the start. Advocates of Evolutionary Architecture will tell you that this is a terrible time to make these decisions – we are at our least informed about our domain, our least familiar with the tools and techniques we’ll be using and our lowest effectiveness as a team. Wrong decisions made at this point often teach people the wrong lesson, driving good developers at successful technology companies into doomed quests to “just get this model right” – refactoring in ignorance instead of getting on with the kind of work that adds value and teaches us how to make good decisions.

Instead, let’s accept our temporary ignorance and optimise our software for changeability so that we can move forward now and change our minds later – when we actually know what we’re doing. To that end I want to share an approach that has helped me in the past – one I’ve borrowed from event-driven architectures and used with great success in a variety of situations. Let’s call it Write Now, Think Later.

Accepting Our Ignorance

A couple of years ago I was working with a bank to deliver a customer-facing web app for tracking the progress of home loan applications. When we arrived, we learned that there was no concept of home loan application status. Home loan applications moved through many different roles and systems, sometimes going backwards or looping around in a complex and poorly-understood process. There were key moments, but people disagreed about what they were and what they meant. Our knowledge of the domain and the systems already in place was poor and we knew it.

The Disaster Waiting to Happen

It had been suggested that we approach the problem with something like this:

bad-idea-at-bank.png

  1. Receive inputs from upstream systems
  2. Determine how each input impacts home loan application status
  3. Persist updated home loan application status

Given our situation, we didn’t have a lot of faith in this plan. We figured if we went down that road, it would go more like this:

  1. Receive a bunch of confusing inputs from upstream systems
  2. Make ill-informed guesses about what they mean and how they impact home loan application status
  3. Update a mutable data store to reflect our probably-wrong understanding
  4. Realise we were wrong about what those inputs meant and despair

That all sounded pretty unpleasant, so we went a different way.

Learning as We Went

We decided that we would avoid mutating state based on probable misunderstandings of our inputs and instead store these inputs just as they were received – without adding any meaning. This way, we could delay inferring meaning until information was actually required downstream, calculating home loan application status on-demand. Our system ended up looking like this:

good-idea-at-bank

  1. Receive a bunch of inputs from upstream systems
  2. We don’t know what they mean, but who cares – store them somewhere with timestamps
  3. Write a service for interpreting these inputs on demand and producing useful information for consumers – we aren’t sure yet but we’ll just make our best guess
  4. Update that service every time our understanding of the inputs and domain changes – no need to write migrations or grieve lost data

We were really happy with our results – our system gracefully tolerated our misunderstandings, adjustments and reversals. It would be easy to think the lesson here is use events – but we didn’t really benefit from events per-se – we benefited from a strict separation between the inputs we received (Write Now) and the meaning they had in our software (Think Later). This principle is an important part of event-driven architectures (it’s why articles about event-driven architecture tell you to name your events in the past tense – to make sure you’re storing what happened instead of storing what you think those events meant at a particular point in time), but you don’t need to use events to start writing now and thinking later.

We only had a narrow range of allowed technologies with this client, but we didn’t need a stream-processing software platform, just an already-approved-at-the-bank Microsoft SQL Server with a single table and an already-approved-at-the-bank SpringBoot API to interpret the events sitting in the database. If you have a little more freedom, you can build something with less operational overhead using the AWS Serverless Application Model. With a couple of small scripts and a little template, you can deploy (and update) DynamoDB tables, Lambdas for writing and interpreting data and API Gateways for receiving inputs from upstream systems and making interpretations of your data available to consumers.

Changing Your Mind Cheaply

A year later, I was on a different team building field operations and scheduling software. We had one upstream source of information – a legacy system sending us work orders. We received the work orders, transformed them into something that made sense to our application and put the transformed work orders into a table, where we mutated their state as they were completed by technicians. It looked like this:

bad-idea-at-metering-company

Getting Stuck

Everything worked fine until we realised we had misunderstood some of the work order data we were receiving. We updated our transformer and had to retransform old work orders and merge in the changes that had been made to those work orders since their previous transformation – but we didn’t know what our changes were! We’d just been mutating our transformed work orders with each change and thereby “pouring concrete on our model” (as a colleague of mine put it). We got through it (a small puzzle for the reader) but it was no fun at all.

Making Change Cheap

Going forward we knew we could solve our problem with events, but we were too far along and too short on time to redesign our whole system – so we made a simple change:

good-dea-metering-company

  • We stored our changes separately, instead of mutating state
  • We transformed work orders on demand without persisting the result
  • We merged changes into our transformed work orders on demand

From that point on whenever we needed to change how we were transforming work-orders, we just changed our transformer and moved on with our lives.

Though we initially performed all of this transforming and merging on-demand, we soon found this wasn’t fast enough for some use cases. Because we were using the AWS Serverless Application Model, it was easy for us to instrument writes to our DynamoDB tables with events and use those events to trigger the transforming and merging of our data asynchronously, storing the results in separate table that could be read quickly and easily by consumers.

We didn’t write any events ourselves, but we were still able to create a strict separation between the inputs we received, the model we presented to consumers and the changes those consumers made. Instead of mutating state based on a point-in-time understanding of how changes ought to impact our data, we wrote everything down as it happened (Write Now) and interpreted it on-demand (Think Later). It worked really well for us and we didn’t need to redesign our entire system to benefit from it.

As in the Back-End, so in the Front-End

We can also apply Write Now, Think Later in front-end development. Redux makes use of event-driven architectural principles, but many Redux users are missing out on some of the biggest benefits. Redux is tricky and we won’t go into detail here, but the part we care about looks something like:

redux

  1. Somebody interacts with a component
  2. An action is dispatched
  3. A reducer notices that action and mutates data in your store

Missing the Point

I’ve seen many implementations of this pattern where actions are used more like commands – a user clicks a button and a component dispatches an action like SaveBananaForm. That means instead of telling our reducer that the banana form’s save button was clicked, we’re throwing that interaction away and issuing commands based on what we think it means at a particular point in time – we’re thinking now instead of later. If we discover that this interaction actually meant something else, we can’t go back and reprocess it because it’s gone. We know what commands we issued but we don’t know why we issued them.

Applying What We’ve Learned

What if we apply Write Now, Think Later and instead of dispatching future-tense commands as actions, we Write Now and postpone our thinking by dispatching past-tense events as actions – SaveBananaForm becomes BananaFormSaveButtonClicked and we don’t worry about what that means until we get to the reducer.

It seems like a small change, but the accompanying shift in thinking is powerful. Say we initially thought that BananaFormSaveButtonClicked meant we should post that data to the server, then we later realise it actually meant we should validate inputs and only then consider posting to the server. We can rewind our actions and store, modify our reducer based on our new understanding of what BananaFormSaveButtonClicked meant and then playback our actions again. Decisions about what our inputs mean are now easily reversed, keeping our options open and allowing us to move forward without too much analysis.

Key Takeaways

These experiences and others have made me watchful for signs that my team might be building inflexible software:

  • Persisting data unnecessarily
  • Meddling with inputs before persisting them
  • Mutating data instead of saving changes separately

If you’re doing these things too, consider how you can Write Now and Think Later:

  • Interfere with inputs as little as possible before persisting them – this way you can change your mind about what inputs mean and how they should be processed
  • Consider the inputs you persist immutable and store the changes you make separately – this way you can change our mind about how changes are applied
  • Interpret data on-demand where possible – this way adjustments to transformation, applying changes etc. will be automatically reflected in existing data

 

Arguing with Kent Beck — August 13, 2015

Arguing with Kent Beck

Just a few years ago, Kent Beck said:

for each desired change, make the change easy (warning: this may be hard), then make the easy change

This sounds very nice, doesn’t it? But having seen it in practice, I don’t think it is so nice after all. Because really, it amounts to inside-out software development. If you spend lots of time making the change easy and only make the change afterwards, you’re postponing the step that will actually tell you whether or not it was the right change to make.

At its core, this feels like a question of inside-out versus outside-in. I was first introduced to this debate at ThoughtWorks University and I’ve been able to observe and experiment with both approaches extensively since then.

Say we’re adding a comment button to a website. Working outside-in, we start by adding the actual button. Then we notice it doesn’t do anything and that prompts us to build something to hook the button up to – at which point, we can investigate what that should be. After deciding to build a controller action, we discover we need some way to interact with comment data and after discussing it with some other people we create a new model. Producing that model prompts us to store the comments somewhere and after a bit of reading, we decide to use our existing database and write a new database migration. Each step follows naturally from the last and presents a clearly defined problem that we can consider, research and discuss. Letting the user-facing part of our feature drive our implementation prompts us to confront questions about exactly how the outside should work very early, which helps us make good decisions about how the inside ought to work. We never build anything we don’t need, because we produce each piece of our implementation only to meet the needs of the previous piece.

Inside-out is very different and for many software developers, it is the default. If we want to add our comment button working inside-out, we’ll start by writing a database migration to store the comments and a model to interact with them – because that’s the pattern we’re familiar with. We’ll write a controller action – because we know that’s where that sort of thing goes  – then finally we’ll put a button in and hook it up. Working this way, we don’t have a failing test or a broken application telling us what to do next. Instead, we have to envision something of the whole solution in advance. Doing this requires experience and a good knowledge of the application – which we may or may not have – and we’ll probably end up applying some pattern we already understand, rather than growing a solution collaboratively as we pair with other developers and as our understanding of the problem develops. We have to make more architectural decisions up front, deciding what cases to handle before we’re even sure what the user interface will make possible. Only at the very end do we see whether all the pieces fit and if we have even built something our users will like, creating the potential for rework.

‘Make the change easy, then make the change’ sounds cool – but it’s inside-out. It requires more expertise, front-loads architectural decisions  (leading to over-engineering), delays feedback (leading to rework) and encourages us to use the patterns we already know instead of trying new things. Sometimes that’s OK, but why risk it?

 

 

Capybara and simple, reliable end-to-end testing — June 1, 2015

Capybara and simple, reliable end-to-end testing

On my last project, we initially had a lot of trouble writing reliable end-to-end tests. Over time, we improved reliability, readability and usefulness by changing the way we approached end-to-end testing and learning more about Capybara.

Let’s look at some loosely-based-on-reality examples. We had a feature that was similar to a web forum. One person could create a post, which other people could see in a list and reply to. We wanted to test that the posts we had automagically created were appearing as expected in the list. Initially, we wrote something like this:

posts = all('.post')
expect(posts[0].text).to eq 'First post heading'
expect(posts[1].text).to eq 'Second post heading'

And so on. This looked neat at first – we could assert that the posts appeared with the correct text in the correct order and that posts we intended to hide were indeed hidden. But there was a problem – we could continue retrieving or rendering posts after invoking Capybara’s all finder, in which case those posts were unintentionally excluded from our results. We needed to wait until all of our posts had been retrieved and rendered, but Capybara didn’t know what to wait for – it doesn’t know how many posts there are supposed to be. The result of the spec depended on how fast our EC2 instance had rendered the text in question, how quickly the page loaded and how long it took to retrieve posts from the API.

Perhaps this approach isn’t well supported because it isn’t really necessary. Does a user in this scenario ever expect the list of posts to explicitly exclude a particular post? If we want to test that, we can test it at a lower level – perhaps by using a posts controller spec to verify that a scope is being applied. The same is true for ordering. Provided you have good unit test coverage, a good approach might be to test a simpler case:

post = find('.post')
expect(post.text).to eq 'Only post heading'

Now that Capybara has a single thing to look for, we can use the find finder to wait for that thing to appear so that our test is less sensitive to variations in timing. You can read more about Capybara finders here. But we might still have problems. If you are using Angular – as we were – sometimes it might take a little time for your text to render correctly. The above approach will wait for the element to exist, but it will compare the actual text to the expected text immediately which may yield inconsistent results. Instead, we could try:

post = find('.post')
expect(post).to have_text 'Only post heading'

Now that we’re using a finder with an implicit wait and a matcher with an implicit wait, Capybara will wait for the element to appear, then wait again for its text to match the expected value. You can read more about Capybara matchers here.

We’ve already made big improvements to the reliability of our tests, but the thinking behind them is still a bit funny. What we really want to test is the interaction – the part that goes end to end, from user to database. We need to find a post on the page only so that we can click on it and read it, just as a user would. We don’t need to meticulously inspect all content on the page – that could be done more efficiently in a view test. What if we just did this?

post = find('.post', text: 'Only post heading')
post.click

But is the selector really necessary? Sometimes you might need to check something very specific, but often that is better done in a lower level test and the specific thing will have specific content and thereby be unique on the page anyway. Capybara has an action for clicking on links, so we might even end up with:

click_link 'Only post heading'

Now we’re using a Capybara action. Capybara actions generally have an implicit wait and help us write more readable, interaction focused tests.

Let’s look at another example, like responding to a post. Perhaps this is a simple case and we can apply our current approach: Find something specific using a finder with an implicit wait, assert on its content using a matcher with an implicit wait and use actions to keep complicated selectors out of our spec. That might look something like this:

fill_in 'Add your response', with: 'Response content'
click_button 'Submit'
expect(page).to have_text '1 response'
expect(page).to have_text 'Response content'

But your case might be too complex for something like that. For example, you might have another submit button on the page. In this case, you might be tempted to go back to a complicated selector:

find('.post .post__new-post-response button').click

But this isn’t necessary. We can use method chaining to scope our calls to Capybara’s actions. For example, we could write a selector for the whole response form component, pull it out into a module or function and then write this:

ResponseForm.fill_in 'Add your response', with: 'Response content'
ResponseForm.click_button 'Submit'

A lot of this falls out naturally from writing end-to-end tests from a users perspective. Our users don’t sleep(1) after they click a button; they don’t give up if their content takes half a second to load; they don’t use CSS classes to decide which button to click or where to look on the page for some text they just entered. Now our specs don’t either.

Here are some key ideas that have been useful to us:

  • Try to write end-to-end tests from the user’s perspective
  • Find and match using Capybara’s implicitly waiting finders and matchers, to avoid timing sensitivity
  • Use actions
  • Keep selectors out of your spec