Introduction
Hey! I’m Aniruddha Shriwant, a Software Engineer at Fyle, where I’ve been working for the past two years. In this blog post, I’m going to share our approach to ensuring smooth migration from AngularJS to Angular, particularly by writing End-to-End (E2E) tests for our legacy codebase.
My colleague Aditya previously shared how we kicked off this migration journey in this blog. Do check it out for additional context!
The Challenge: Testing Legacy Code
As mentioned in Aditya’s blog, our migration journey from AngularJS to Angular has been ongoing for the past two years. Now, we’re finally approaching the finish line - reaching the most critical and complex areas of our web application.
However, we encountered a significant challenge at this stage. Our legacy AngularJS codebase didn’t have any unit tests in place, which meant every feature or change had to be manually tested. This made it extremely difficult to confidently migrate critical sections without introducing regressions or missing subtle functionalities.
To tackle this effectively, we needed a reliable way to ensure that nothing broke during the migration.
Why Choose End-to-End (E2E) Testing?
Typically, in the testing pyramid, unit tests form the base and are prioritized because they’re fast, inexpensive, and provide immediate feedback. End-to-End tests, on the other hand, sit at the top - usually fewer in number due to their higher cost in terms of execution time, CI resources, and maintenance complexity. However, when dealing with legacy code that lacks existing unit tests, this pyramid gets inverted. Here, prioritizing E2E tests makes sense, since they quickly verify critical user interactions, offering immediate assurance that essential functionalities aren’t broken during major changes like migrations.
With this perspective, we decided E2E tests were our best bet to confidently manage our Angular migration.
Our Approach to Writing E2E Tests
Documenting Feature Requirements (FRs)
We started by exhaustively identifying and documenting every feature and edge case supported by our legacy codebase. Since we previously had no documentation, this step became crucial. These documented FRs served as a reliable source of truth, something we could always refer back to.
My colleague, Aiyush, played a pivotal role here - his extensive experience at Fyle helped us clearly document critical functionalities and, in many cases, understand why certain features existed in the first place. By the end, we had nearly 150 feature requirements documented!Grouping FRs into User Testing Scenarios
Given the large number of FRs, individually writing an E2E test for each wasn’t practical. Instead, we grouped related FRs into cohesive user flows and scenarios. This helped us efficiently cover multiple FRs within fewer tests. Moreover, grouping improved our Continuous Integration (CI) pipeline efficiency, as E2E tests are relatively slower to execute and consume more CI resources.
Writing the E2E Tests
Finally, we translated these user scenarios into E2E tests that closely simulated real-world user interactions.
With this structured approach, we ensured comprehensive and practical coverage of our most critical user journeys.
Few best practices we followed while writing E2E tests
Here’s how we implemented some best practices to write reliable, maintainable, and effective E2E tests, We used Playwright - a testing tool that helps automate browsers and simulate real user interactions, making it ideal for writing reliable End-to-End (E2E) tests.
Prioritizing User-Centric locators
When writing E2E tests with Playwright, there are generally two ways to select DOM elements:
Component or CSS selectors:
// Clicking the save button using a component-specific CSS class:
page.locator('.save-cta').click();
User-facing selectors (recommended approach):
// Clicking the save button as a user would identify it:
page.getByRole('button', { name: 'Save', exact: true }).click();
We strongly preferred the second approach—user-centric locators—because they’re less brittle. Component or CSS-based selectors tend to break easily whenever component names or CSS classes change, which was common during our migration from AngularJS to Angular. By contrast, user-facing selectors rely on elements the user actually sees and interacts with, making our tests more resilient and maintainable.
This approach aligns directly with Playwright’s recommended best practices.
Grouping and Structuring Tests into User Flows
Given our extensive list of Feature Requirements (FRs), individually testing each scenario would have been highly inefficient. To address this, we grouped related FRs into coherent user flows, typically covering 7-8 FRs within a single test scenario.
Example scenario: testing various form inputs tests in one flow:
Verify that specific input fields appear by default.
Check for mandatory field errors when attempting to save without providing the required details.
Ensure dropdown fields load and display default selectable values.
Confirm date input fields validate correctly, especially for future dates.
Validate that configurable labels and placeholders correctly reflect user-defined settings.
Minimizing API Mocking to Prioritize Real Integrations
Playwright provides an easy mechanism to mock API requests. However, we consciously minimized the use of mocks in our E2E tests. Instead, we prioritized genuine integration with our backend services
This approach allowed us to:
Test the complete flow: from creating real backend resources (such as projects with category restrictions) to verifying user interactions like saving an expense form.
Detect issues early: Not just frontend regressions, but also subtle backend bugs or unexpected behaviour changes in APIs.
Of course, there were cases where mocking became necessary—for instance, when backend APIs operated asynchronously or had slow responses. In these scenarios, selective mocking allowed us to maintain test stability and speed, effectively balancing ideal test conditions with practical constraints. By thoughtfully minimizing mocks, we increased our confidence that the E2E tests genuinely reflected the end-user experience, capturing issues across both frontend and backend layers.
Early Impacts and Anticipated Benefits
Through our structured approach to E2E testing, we’ve already started seeing some encouraging results:
Detecting existing bugs early: We’ve uncovered a long list of hidden issues while writing our E2E tests—bugs that would have otherwise gone unnoticed until production. 😛
We’re also excited about the future benefits that we anticipate from continuing this strategy:
Rapidly identifying missed behaviours or unexpected changes introduced during migration.
Greater confidence in releasing major updates to the most critical parts of our web app.
Achieving a smooth, successful transition from AngularJS to Angular, finally allowing us to say goodbye to our legacy codebase! 👋🏻
I’d love to hear about your experiences—have you faced similar challenges or successes with E2E testing in legacy code migrations? Let me know!
Special thanks to my teammate Suyash for closely collaborating on writing these E2E tests and helping us confidently prepare for a smooth migration! 🚀
Great read Aniruddha! 👏
Great read Aniruddha!
I'd also like to add how effortless it is to generate end-to-end tests in Playwright. You simply open the website, interact with it naturally, and let Playwright automatically generate the corresponding test code—eliminating the need to write everything manually, line by line.