Design a full-stack project by starting with its functional tests. Does that sound crazy?
In the sequence of articles, I'll demonstrate how to design the testing of a Full Stack project alongside the project's design itself, using a simple example. I'll also highlight the benefits of this approach.
The source code of the example project is here
Maybe this article will be helpful for Front-End/Back-End developers and automation test developers (or anyone else looking for a good night's sleep)
Defining Functional Testing
Many books and articles have been devoted to testing and its importance, so I'll skip that topic here. Instead, this article will focus on functional testing and its role in product design. Functional testing evaluates a product's functionality without relying on its internal implementation. For a more precise definition, you can refer to Wikipedia.
Why Do You Need Functional Testing?
During my career as an automation engineer, I have encountered many projects that were launched without functional tests from the outset. This stems from the misconception that functional tests should be added to a finished product and serve only for quality assurance. Without tests, the number of bugs and client complaints grows. Developers then rush to add tests, only to realize that adding functional tests to an existing product is not as easy as they thought.
The difficulty in adding functional tests at this stage lies in the product's lack of testability. When the product was designed, no one considered its automated testing needs.
Testability of a product can include:
- The ability to run the product locally (e.g., building the product/environment from source code for each test run)
- A complete product API for automated testing (e.g., automatic creation/deletion of a product's user/account for each test run)
- The ability to implement mocking of product components or functionalities (e.g., replacing the product's database with a mock or emulator)
During the product design phase, it's much easier to design tests because you're not constrained by existing code, frameworks, or utilities embedded in the product's architecture.
In this article, I'll demonstrate how a simple web application can be designed (and implemented) using functional tests design.
Defining the General Design and Objectives
Let's define the main purposes of the project: user registration and presenting personal info after sign-in.
More specifically, the project should support:
- User registration
- Sign-in for registered users
- Displaying user's personal info
The design should consist of two main parts:
- API - for user administration and storing personal information
- Web - for registration, sign-in, and displaying user info
Notice the decoupling of the storage into a real instance and its mock. This enhances the product's testability and independence from any actual storage instance. As you might guess, this follows the Adapter design pattern.
Initial Design Steps
Let's begin with the Web part of the project. Why start here?
The Web part is the user's interface, helping us define the project's detailed purposes from the user's perspective (user stories). It also aids in determining the API part's requirements, as the Web part interacts directly with the API.
Test Definition and Implementation
Let's define a set of tests for each project purpose.
Registration tests:
- User successfully completes registration with valid personal data
- User fails registration when using invalid personal data (e.g., missing last name)
- Existing user cannot register again
- Registration fails due to server-side errors
Sign-in tests:
- Existing user can sign in successfully
- User's personal information is displayed after sign-in
- User cannot sign in with invalid credentials
- Sign-in fails due to server-side errors
These tests provide a detailed description of the project's general purposes. Implementing functional tests helps us understand the required web pages, their functionalities, user interactions, and the interconnection between the Web and API parts.
Each test follows the AAA (Arrange-Act-Assert) pattern:
- Arrange - set up the test's precondition
- Act - perform the action being tested
- Assert - verify the result of the action
Our tests will interact with web page models (which correspond to real project web pages) and API server mocks.
Frameworks used (other similar ones can be substituted):
- ExpressJS - web server
- Playwright - functional tests
Components involved in testing the Web part:
The Example of the Registration page test run (sequence version):
Now, let’s examine the code for the tests we defined earlier:
- RegistrationPage — the functional model of the registration page (also known as its Page Object Model or Page Object)
- RegistrationSucceededPage — the functional model of the successful registration page
At this stage, we can’t implement the methods called in the tests because the actual web pages don’t exist yet. The same applies to the mock functions used in test preconditions, as we don’t know the API server’s requests and responses yet.
These methods and functions will be stubbed (learn about stubs here), initially returning invalid results to intentionally fail the tests.
So, what do we have now and how does it help?
- An understanding of the real web pages’ functionality (through the test code)
- A set of functional tests for the project’s web pages
Let’s Code it Up!
The project items still not implemented:
- web server(using ExpressJS)
- project web pages
- API server interconnections(in the web pages)
- stubs replacing by code in the Page Objects and in the mocks of the API server
I wont waste time on the describing of the web pages creation and the configuration of the web server. All the relating code is here. Just some examples
Web server (server.ts):
Registration page example (register.html):
The link between the registration page and the API server(register.ts):
The appearance of the registration page in various scenarios
During the development of the web pages, test stubs will be replaced with actual code. The same applies to mocks used in test preconditions.
Below is an example of implementing mocks using Playwright (the full code is here):
Once all tests pass, the web portion of the project can be considered complete.
Summary of the Web Part Development Process
This article outlined how to design the web portion of a project, starting from broad concepts and progressing to detailed components. During the web development process, requirements for the API server were defined, which will assist in its subsequent design.
The understanding of the web’s functionality was achieved by creating functional tests, followed by the parallel development of the web components alongside the tests. The functional tests run independently of the API server (which, at this stage, hasn’t yet been implemented).
This approach mirrors a gradual progression from general concepts to specific details, allowing better control over the entire design and development process while maintaining quality and a clear understanding. It enables the definition of essential project elements before their actual code implementation.
Overall, this process resembles the TDD (Test-Driven Development) methodology, applied here in the context of functional testing.
One drawback is the extended development time, which can be critical for startups where delivering features quickly may take precedence over quality. As a result, startup managers often remain skeptical of this method.
However, regular practice of this approach can reduce development time as experience and intuition grow.
The code for the web portion of the project is available here.
The next article will cover the API implementation and deployment of the entire Full Stack project.
Thank you!
Top comments (0)