Want to join a thriving community of quality champions? | Check out Shiftsync

End-to-End Testing vs Integration Testing

You want to test your application properly but don't know which testing strategy to go with? Kudos to you for…

By Testim,

You want to test your application properly but don’t know which testing strategy to go with? Kudos to you for realizing you need automated UI testing.

As you may already know, the three most common types of software testing are unit, integration, and end-to-end (E2E) testing. However, in this post, I will focus on end-to-end testing vs. integration testing, which are higher-level tests than unit tests.

If, however, you’re interested in understanding unit testing as well, check out this post.

Now, let’s start with a bit on the fundamentals.

Why Do We Test?

When building new software, I personally write tests for two simple reasons:

  • To save time
  • To write better code

When I started as a developer, I hated tests. What really got me hooked is when I realized how my code’s quality improved as I began introducing automated tests. Testing your own code also increases your personal responsibility for the code.

Regarding saving time, the earlier you find bugs, the faster and cheaper it is to fix them. Studies have shown it to be as much as 100X more expensive to fix a bug that escaped to production as it is to fix a bug you found in the design and coding stage.

Integration Tests in Software Development

In contrast to unit testing, in which you test a small isolated unit, integration testing usually involves testing a particular functionality—usually referred to as a module—that has dependencies on another functionality (e.g., a function calling another function). The goal of these tests is to check the connectivity and communication between different components of the application.

Integration Testing Objectives

There are three main objectives to running integration tests:

  • Integration tests ensure synchronization between modules when they work together to accomplish a specific task, especially as these modules could have been built by different developers or teams.
  • These tests also help validate the application’s interfaces so that the data flowing from one module to another is appropriate.
  • They ensure that the connectivity between modules works as it was conceived to work.

Integration Test Example

Consider this simple example of an integration test (quite possibly the simplest you’ll ever see):

const HelloName = (name) => {
return `Hello ${name}!`
}

const checkAndSayHello = (name) => {
if (!name.trim()) {
return false
}
return HelloName(name)
}

test('should generate a valid text output', () => {
const text = checkAndSayHello('Testim')
expect(text).toBe('Hello Testim!')
}); // end of test

In this test, we’re checking the integration of two modules: checkAndSayHello and HelloName, with the former having a dependency on the latter. HelloName is a function responsible for printing out the text, while checkAndSayHello adds some validation by making sure the name provided is not an empty string.

Approaches to Integration Testing

Now that we know the purpose of integration testing and have seen an example let’s see how we can implement it.

Depending on your project size, team, budget, and other factors, different integration testing approaches could work better for you. Here’s a brief overview of the three most popular approaches.

The Bottom-Up Approach

In this first approach, the bottom-up approach, testers focus on integrating smaller modules that are usually built first into bigger ones that could be built later. Running these tests requires that higher-level modules be simulated—these simulated modules are called drivers.

Less complex modules are then progressively integrated into bigger ones right up until the application is complete. The main advantage here is the ability to fail fast, as smaller modules are easier to detect and quicker to fix.

The Top-Down Approach

The opposite of the bottom-up approach is the top-down approach. Here, priority is given to the larger, more complex modules, and the lower-level ones are simulated. In this case, the simulated modules are called stubs.

You can learn more about stubs and drivers here.

The Big Bang Approach

Finally, we have the big bang approach. With this approach, all modules are tested together at the same time. Of course, this means that all modules have to be ready. As such, this can be a very time-consuming approach. Also, detecting errors can be a bit more difficult compared to the other approaches. But if your application is small, the big bang approach can be a great go-to.

Benefits of Integration Tests

Overall, I will say that integration testing is a must. As someone who spends a lot of time writing these types of tests, I can tell you that they will add value to your project in terms of the confidence they bring. Some benefits are that

  • They make sure modules work together and meet expected standards.
  • Since integration tests are higher level (than unit tests), they tend to improve the application’s overall test coverage.
  • Integration tests lay the groundwork for higher-level tests like E2E testing.

End-to-End Tests in Software Development

Funny enough, E2E testing looks very much like manual testing. When doing E2E tests, you are looking to test the product the same way a real user experiences it. You want to make sure everything works as expected (functionality and performance). But, again, this is all done in an automated manner.

Testing Objectives

There are two main objectives to E2E testing:

  • E2E tests simulate a user’s step-by-step experience.
  • They also allow you to validate different subsystems and layers of the application.

End-to-End Test Example

Consider this test for an app login workflow:

test('login test', async () => {
  await go("http://demo.testim.io");
  await resize({
    width: 1024, 
    height: 680
  });
  await click(l("LOG_IN"));
  await type(l("[tabindex='1']"), '[email protected]');
  await click(l("[type='password']"));
  await type(l("[type='password']"), 'hau@8f!@#hD');
  await click(l("[form='login']"));
  await waitForText(l("HELLO,_JOHN"), 'Hello, John');

}); // end of test

In this E2E test, we simulate a user, John, going to the app homepage at demo.testim.io and then clicking the Login button.  John puts in his email and password and is redirected to a welcome page, which greets him with the text Hello, John. Awesome and simple!

Now, let’s see how to approach this seemingly more interesting kind of testing.

Approaches to End-to-End Testing

Horizontal E2E

When most people refer to E2E testing, they usually are thinking of horizontal E2E. Horizontal E2E tests usually replicate typical application use cases and then go through them from start to finish. A good example is the registration process for a new user.

Vertical E2E

Vertical E2E testing is a more technical approach that aims to test components of the system by following data through the UI, API, and database layers. The best example that comes to mind here is the checkout process on an e-commerce website.

As you can see, there’s an overlap between these two approaches to E2E testing, and that’s because you need both to successfully craft tests that truly go from end to end.

Benefits of End-to-End Tests

Looking at this, you can probably see why E2E tests can even be considered the most important type of test. Some of the benefits are

  • They make sure everything is working as expected, from UI right through to the database layer.
  • Usually, they sit on top of integration tests and therefore increase your app’s overall test coverage—the more testing types you apply, the better.
  • They help detect bugs before end users do.

However, all of this precision and value comes at some cost, as E2E testing is more complex and requires more time and resources. Luckily, with the continuous improvement of E2E testing tools, this is becoming less worrying. Tools like Testim make E2E tests faster to write, nonflaky, and easy to maintain.

End-to-End Testing vs. Integration Testing

So, in a nutshell, here’s what we know about end-to-end testing vs. integration testing:

Integration Testing End-to-End Testing
Testing to make sure app components work well together. Testing the product as a user would experience it.
The scope could span multiple components but won’t span the entire stack most of the time. Testing scope is wider and spans the entire application technology stack.
Done to discover connectivity issues between components when they are working together. Done to have a feel of the user experience of the app.
Less expensive to implement. More expensive to implement, both in terms of hardware and software.
Higher-level than unit testing. Higher-level than integration testing.
Faster to perform. Slower to perform.

So, Which Testing Strategy Do You Need?

By now, you may have already come to this conclusion: It’s not a question of end-to-end testing vs. integration testing, but rather E2E testing together with integration testing. Even though each type of testing takes a different approach, they share the common task of checking if the result of some code execution meets a certain expectation. And in that way, they are complementary and together can offer you the full confidence you need in your software’s performance.

If you want to take your testing strategy to the next level, try Testim’s Automate.

What to read next

UI Testing: A Beginner’s Guide With Checklist and Examples

What Is Test Automation? A Simple, Clear Introduction