A guide to functional testing

Functional testing is the verification of a system's compliance with functional requirements. 

Testing can be divided into two major groups: functional and non-functional testing. Functional testing includes checks of the functionality of an application, software, or system. Non-functional testing includes checks of security, performance, compatibility, and so on. 

Without functional requirements for the system, it is impossible to conduct functional testing because we do not know what functions and what behavior our system should provide. 

Functional requirements are part of the project specification or documentation that describes the functions, tasks, or actions that the system or product should perform. They define what capabilities and features should be included in the system under development. Functional requirements are often described in terms of specific operations, user actions (e.g., through user stories), or interactions between system components. Functional requirements may look like this:

  • The “add” function should take in two numbers and output the result of their addition
  • User registration: The system should provide users with the ability to register by entering their personal information.
  • Adding items to cart: In an online store, users should be able to add items to their cart by clicking the "Add to Cart" button.

The main goal of functional testing is to verify that the system performs the functions that were specified in the requirements. A side goal of functional testing is to find functional bugs. However, it is important to note that bug hunting should not be the sole purpose of functional testing. Primarily, functional testing is aimed at increasing confidence in the quality and confirming that the product or system works as intended. 

Functional testing is mainly performed by functional testing specialists (QA engineers) or developers when writing unit tests. According to one of the principles of testing, the product should be tested by someone other than the person who developed it.

Table of contents

Functional testing vs non-functional testing

Functional testing involves checking for compliance with functional requirements. However, a system may have non-functional aspects of quality that are also subject to requirements. 

Non-functional aspects of quality are provided by non-functional types of testing, such as performance, usability, security, maintainability and other -ility aspects of quality.

If we refer to the testing quadrant from the book "Agile Testing," functional tests are conducted in the second quadrant (Q2), they help support developers’ teams and verify business requirements. While non-functional tests "criticize" the already created product and belong to technology-facing testing.

Image source

Examples of functional tests:

  • Function “add” takes test data “a” and “b” as input and gives the result “a+b” after running. 
    • def test_add_positive_numbers(self):
    • result = add(3, 5)
    • self.assertEqual(result, 8) (example of a unit test)
  • Being logged in: When you go to the main page of the site and click on the icon of Personal Account the expected result is that you are passed to the page with Personal Account
  • When on a product page, you click on the "Buy" button. The expected outcome is that the product is added to the cart, the counter on the cart changes from 0 to 1.

Examples of non-functional tests:

  • If you are logged into the system, the icon of the personal account is large enough to click (usability)
  • The page with the Personal Account opens no more than one second before the first element is rendered. (performance)
  • Animation of moving a product to cart on the product page is clear and happens smoothly and noticeably for the user. The icon of the number of items in the cart has a bouncing animation. (usability)

Types of functional testing

Functional testing is divided into types that depend on things such as: functionality development stage, level of testing pyramid, code access levels, and testing scope. 

For this article, we are focused only on types of tests as they pertain to functional testing. So while unit tests can also be part of performance testing, for example, we will only explore tests from the functional testing perspective. 

Testing Pyramid

Functional testing is divided according to the testing pyramid, depending on the levels.

Unit testing

The testing process involves writing automated functional tests on functions and methods. Unit testing is the lowest level of the testing pyramid. Unit tests are generally written by developers. There is also a test driven development approach, which implies development through unit tests.

Integration testing

Integration testing is the next level of the testing pyramid. It means functional testing of two or more modules of a system under test between each other. Testing is primarily focused on data exchange between these modules.

End-to-end testing

This is end-to-end testing of the whole system/product. It is closest to user scenarios and checks interactions of all system components. This type of test is the most expensive in terms of resources, but gives maximum confidence in a good user experience

Scope 

Functional testing can also be categorized by the scope of functions being tested.

Smoke testing

A type of testing that is performed to verify that critical functional parts of the system work as intended. Smoke tests check only the critical functionality. It is an enabler for other types of testing, for example, it is performed before regression testing. 

Sanity tests

This type of testing focuses on thoroughly examining the specific functionality being tested, while smoke testing aims to quickly cover a broad range of functionalities. Sanity testing typically occurs after smoke testing to verify all aspects of the newly added functionality.

Regression tests

These tests сonfirm that recent changes in the product code have not had a negative impact on the existing features. Regression tests are often performed when sanity testing has already been done and no new changes are being made, usually before a product release.

How to perform functional testing

Functional testing consists of several stages. The basic steps to perform functional testing are:

Test analysis

Test analysis answers the question, “what to test?” At the initial stage it is necessary to review functional requirements to the system and familiarize yourself with documentation of new functionality provided by analysts or customers. Test analysis includes analyzing the test basis to identify testable features and to define and prioritize associated test conditions. Potential system failure points should also be identified during test analysis. 

Test design

Test design answers the question "how to test?" After familiarizing yourself with the documentation, you can start creating test cases. This activity often involves the identification of coverage items, which serve as a guide to specify test case inputs. 

A test case is a set of input values and preconditions, a set of steps, and an expected result. Occasionally, a post-condition may also be required. It is important that a functional test case tests only one function and is understandable and reproducible.  

Test design techniques can be used to support this activity. 

  1. Equivalence class testing: This testing technique divides input data into equivalent groups or classes to reduce the number of test cases. If a program is expected to process values within a certain range, testing just one value from each group is often sufficient.
  2. Boundary value testing: This technique focuses on testing boundary values to reduce the likelihood of errors at the edges. Instead of testing all values within a range, testers concentrate on the minimum and maximum values, as well as values close to the boundaries.
  3. Decision table testing: This method is used to test various combinations of input conditions. A decision table consists of all possible combinations of input data and corresponding expected results, allowing testers to identify logic problems in the program.
  4. State transition testing: This technique is applied to test systems that transition from one state to another in response to specific events or actions. Testers create state transition diagrams to identify all possible state transitions and ensure the program functions correctly in each state.
  5. Pairwise testing: Pairwise testing efficiently covers all possible combinations of input data using the minimum number of test cases. Instead of creating tests for every combination, testers select the most important parameters and test them pairwise.
  6. Error guessing: This testing method involves testers using their experience and intuition to guess potential errors in the program. Based on their knowledge of previous errors and experience, they create tests that can uncover potential issues.

After forming the basic scope of test cases, they are prioritized and combined into test suites. A test suite is a sequential set of test cases grouped by priority, test areas, positivity and negativity, or risk areas.

Test execution 

Before test execution you should first make sure that the test environment is stable and there are no upcoming plans to make changes to the product. Test execution includes running the tests in accordance with the test suites. Test execution may be manual or automated and the test results are logged. Actual test results are compared with the expected results. Anomalies are analyzed to identify their likely causes and then logged as bug reports.

Test completion

At the end of testing, a test report is provided detailing which tests were performed, which tests were passed successfully, and which bugs were found. The test report is a necessary artifact because stakeholders use the report to decide whether to roll out the functionality in production or to make the required improvements and perform another iteration of testing.

Functional testing examples

I’ll walk you through an example of how I would perform a functional test for a health insurance website. 

First, there are system requirements. The requirements state that there should be a page including a form where users can input their age. The cost of health insurance should be calculated based on the age: $10 for 17 and under, $50 for 19 up to retirement age, and $60 for retirement age and older. 

Next, I do test analysis. I go to the product manager to determine which age ranges I will calculate insurance costs for, asking questions such as whether or not it is possible to insure babies from 0 years old and what age is considered “retirement age.” The product manager also informs me that the new insurance calculation feature will live on the website homepage and will only work on the desktop web version. 

Then, I move into test design.  Based on the provided system requirements, I can develop several test cases to ensure that the health insurance cost calculation functionality works as expected. Here are some test cases:

1. Boundary testing: Age below 18:

  • Input: Age = 17
  • Expected output: Insurance cost should be $10.

2. Boundary testing: Age 19:

  •  Input: Age = 19
  • Expected output: Insurance cost should be $50.

3. Equivalence class testing: Age above 18 and below retirement age:

  • Input: Age = 25
  • Expected output: Insurance cost should be $50.

4. Boundary testing: Retirement age

  •  Input: Age = 64
  •  Expected output: Insurance cost should be $50.

5. Boundary Testing: Age above retirement age:

  • Input: Age = 65
  •  Expected output: Insurance cost should be $60.

  1. Edge case: Age input validation (babies):
  •  Input: Age = 0
  •  Expected output: Insurance cost should be $10.

8. Edge case: Age input validation (negative scenario):

  •  Input: Age = -5
  •  Expected output: System should display an error message indicating invalid age input.

9. Edge case: Age input validation (non-numeric input):

  • Input: Age = "twenty"
  • Expected output: System should display an error message indicating invalid age input.

10. Edge case: Age input validation (age with decimal):

  •   Input: Age = 25.5
  •   Expected output: System should display an error message indicating invalid age input.

11. Edge case: Age input validation (greater than the acceptable age):

  •  Input: Age = 999
  •  Expected Output: System should display an error message indicating invalid age input.

After test design comes test execution. The developers provide me with a test build of our web application with the new changes. I start going through the test suite and during the run I realize that test number 4 has an error and gives the cost of insurance as $60 when it should show $50. After analyzing the form with additional values, I find that the equivalency class boundary for the retirement age is set to 60 rather than 65, so I submit a bug report to the bug tracking system detailing this error. 

For test completion, I provide information about the bug and the stakeholder decides that it is a blocking bug that needs to be fixed. After the bug is fixed, I do a confirmatory test with a resolution. The form is now showing the expected outcomes and the bug is fixed. 

In addition to the tests performed, the following action items are identified for implementation in the future for similar tasks: It is necessary to explicitly specify boundary values in requirements instead of implicit ones. Also, because of plans to further improve this form, the team will develop unit tests for boundary values for regression testing.

Manual vs. automated functional testing

The choice between automating or manually running test cases is always important, and it depends primarily on the stage of the project. If the product is brand new and likely to go through various changes as it searches for its place in the market, then automation is not the ideal solution. As the product changes, the autotests will have to be constantly updated and the time spent on test support will grow exponentially. 

In another scenario, when we already possess an extensive regression suite of functional tests, and the regression testing duration extends to days or weeks, a decision needs to be made regarding the automation of the most time-consuming and resource-intensive tests.

Sometimes, functional test automation is either impossible or will be very expensive. Therefore, it often makes sense to cover these needs with manual testing. If automation makes sense for your project, it’s worth using frameworks that help you speed up the automation set up such as Selenium, Appium, or Jenkins.  

Selenium is one of the most popular open-source automated testing frameworks for web applications. It supports multiple programming languages such as Java, Python, C#, and more. Selenium WebDriver allows testers to automate interactions with web browsers.

Appium is an open-source automation tool for mobile applications, supporting both Android and iOS platforms. It allows testers to write tests using the WebDriver protocol for native, hybrid, and mobile web apps.

Jenkins is an open-source automation server that helps automate various stages of the software development process, including building, testing, and deploying software. It supports integration with numerous testing and deployment tools, making it a versatile choice for continuous integration and delivery (CI/CD).

To choose between automated and manual testing, you need to rely on the context of your project and consider all parameters. It’s a good idea to use manual functional testing for new functionalities, while regression and smoke testing can be covered by automation.

Best practices for effective functional testing

Embrace shift-left testing. Start functional testing as early as possible! Requirements analysis at the earliest stages of the project and static testing can bring a lot of benefits. Before starting functional testing, it's essential to have a thorough understanding of the functional requirements of the software system. Clear and comprehensive requirements documentation is crucial for designing effective test cases. Collaboration and communication with stakeholders — including developers, product owners, and business analysts — throughout the testing process can help clarify requirements, address concerns, and ensure alignment on testing priorities.

Perform test case review. Develop detailed and clear test cases based on functional requirements using test design techniques. Test cases should cover various scenarios including positive, negative, and boundary cases to ensure comprehensive test coverage. Test design is the most important phase of the functional testing process, so pay close attention to it. Include test case review to eliminate human error. Even experienced testers can make a mistake while creating a complete set of tests for a functionality. You can help avoid these mistakes by performing additional external validation.

Prioritize test cases and risk-based-testing: Prioritize test cases based on risk, complexity, and criticality of the functionality being tested. Focus on testing critical and high-risk areas first to mitigate potential issues early in the development cycle.

Implement test automation. Consider automating repetitive and time-consuming test cases to improve efficiency and reduce manual effort. Automation tools can help execute tests faster, increase test coverage, and provide quicker feedback on software changes. Conducting automatic regression testing after new changes is a strict necessity on any project.

Clearly document and label bugs. When bugs are identified during testing, document them clearly with detailed information, including steps to reproduce, expected behavior, and actual behavior. Clear bug documentation helps developers understand and fix issues efficiently. Label bugs according to the location found and the cause of the bug. This will allow you to analyze and highlight the most buggy parts of the product in the future, as well as identify weaknesses in the testing process in order to prevent bugs in the future.

Focus on continuous improvement. Continuously evaluate and improve the functional testing process by collecting feedback, identifying areas for enhancement, and implementing best practices learned from previous testing cycles. Adopting a mindset of continuous improvement helps optimize testing efficiency and effectiveness over time.

Functional testing plays a critical role in quality

Functional testing ensures that a system’s functions comply with specified requirements and focuses on verifying intended behavior rather than bug hunting. It’s an important aspect of quality assurance because it centers around ensuring software and products function as intended for consumers.