How could Unit Testing impact the development of your solution?

Category:
Software Development
Yaroslav Zhurbilo
iOS Developer
Head of Front-End Chapter
Human Resources Manager
Business Development Manager
Business Development Manager
Business Development Manager
Business Analyst
Business Analyst
Project Manager
Project Manager
UX/UI Designer
IT Lawyer
Chief Technology Officer
Chief Executive Officer
Head of Back-End Chapter
Head of Mobile Chapter
Marketing Manager
Head of QA Chapter
Head of Mobile Chapter

In this blog post from the custom software development series, we are going to answer the FAQ about Unit Testing, as well as its importance for web and mobile development process. Stay tuned!

What is Unit Testing?

Unit tests are automated tests that are run to validate a unit of work  ensuring that it behaves as intended. This helps to detect and offer assurance against bugs in the future. 
Unit of work - a piece of code that is logically isolated in a system. Usually, it is a method or property.

In the following code snippet, you can see the simplest test case with validation of user’s full name:

/// A test case to validate our logic inside the `User` model.
class UserTests: XCTestCase {
   
    /// It should correctly return User's full name.
    func test_userFullName() {
        // Arrange
        let sut = User(firstName: "John", lastName: "Doe")
        // Act
        let actualFullName = sut.getFullName()
        // Assert
        XCTAssertEqual(actualFullName, "John Doe")
    }
}
/// A simple struct of the User
struct User {
    let firstName: String
    let lastName: String
   
    /// - Returns: the full name of the User
    func getFullName() -> String {
        return firstName + " " + lastName
    }
}

Why is Unit Testing needed?

When a code for application is written, it goes through a writing cycle to check code ensuring that it works as intended. But, as the codebase grows, it becomes harder to test the entire application manually. Automated testing allows us to perform these tests. 

As an example, if we wanted to detect a bug, we would need to perform manual testing or simply wait for the next bug to appear and then receive feedback from the Customer or crash service. As a project grows, manual testing may take considerably more time and it is crucial to many projects that they don't have bugs appear during production. Unit tests are oftentimes the first to be skipped when a deadline is coming. But not in our practice.

Unit Testing saves time and money

Steve McConnell, in his book, “Code Complete” (Microsoft Press, 2nd edition, 2004) includes a table that details the cost of fixing bugs at different product development stages. The table also correlates that the earlier a defect is detected, the lower the cost of its correction.


Cost of bug fixing at different stages of software delivery


When unit tests are written, a host of bugs can be found at the software construction stage. This may prevent the transition of these bugs to successive stages, which also includes post product release. This saves the costs of fixing the bugs later in the development lifecycle and also provides benefits to end-users, who may not want to deal with a product laden with bugs.

Unit Testing provides documentation

Developers can use unit tests to have a clearer picture of the module logic and the system as a whole. If there are new developers on the project or multiple developers that work on different features, they may be unaware of these features, they can simply reference the unit tests without spending much time explaining to each other how it should work.

Unit Testing forces to write clean and maintainable code

Something to remember is that good tests can't be written on top of a poorly designed API. On the other hand, well-designed API’s makes it much easier to write good unit tests.

Unit tests are an invaluable tool for making clean and maintainable architecture. When writing unit tests it may become difficult due to having too many dependencies, a sign that the module has too many responsibilities. If you find yourself updating a lot of tests in order to accommodate a small change in an unrelated class, it may be a symptom of tight coupling between modules. Testing your module in isolation encourages you to depend on abstractions, so use well-defined interfaces and favor loose-coupling between modules.

When and what to test

Before we start writing tests we need to decide what to test. Most applications may contain a great amount of code. There is no need to test every piece of code. When we have decided what to test, we need to be focused on what actually matters to the intended users and our application.

What should we test

The most important rule is to test and cover code which is a greater part of the business logic.

We may also have to write tests before we can refactor something and for code that has been repeatedly changed.

What shouldn't we test 

External frameworks and Facades (classes that talk to the external frameworks or wrapping them) are not unit-testable. This is why the classes that talk to actual external frameworks should be as thin as possible

UI elements we can't unit test, and these should only be abstracted away so that they contain as little logic as possible.

If models don’t have measurable behaviors or outcomes, we don’t test them as a rule.

We don’t unit test constructors and private methods directly.

Complex multi-threading code, Database Persistence and the units that need to interact with other units are not unit-testable. It is usually advisable that they be tested utilizing integration tests.

Integration testing means checking if different modules are working fine when combined together as a group. When writing integration tests, we want to verify how multiple units actually interact with each other. Rather than mocking everything outside of a single unit, we instead use real objects to perform our tests and verify their outcomes. The benefit is that we might be able to catch bugs and problems that only occur once that real integration happens.

Unit Testing theory

Prefixing a function in an XCTestCase class with “test”, tells Xcode that the given function is a unit test. Also, test functions shouldn't be private, without parameters and don’t return anything

FIRST principles for Unit Testing

The acronym FIRST describes a concise set of criteria for effective unit tests. Those criteria are:

  • Fast - Unit tests should be fast otherwise your development time may slow.
  • Isolated - Tests should not be made to set-up or teardown for one another.
  • Repeatable - The same results should be obtained every time a test is run.
  • Self-verifying - Each test must determine whether the expected output is likely or not.
  • Timely - Unit tests can be written at any time.

AAA principles for Unit Testing

Each test should be organized into three steps:

  • Arrange the inputs necessary for the system under a test.
  • Act on the system under test.
  • Assert the expected behavior.

Examples of this principle can be seen in code snippets in this article.

Your solution deserves the highest quality.
We know how to make it happen. Contact us and we get back to you soon.

Dependency injection

When we write unit tests, just one method is needed to test our application. If our method relies on another class/variable there should be a possibility to inject that method into it. This may be when dependency injection comes in handy, this allows us to inject objects into our classes to change the output of the testable unit.

Dependency injection is when our testable object has a dependency on another object.

According to Wikipedia, “In software engineering, dependency injection is a software design pattern that implements inversion of control for resolving dependencies. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it.”

Here is a simple example of a unit test with a testable object that has a dependency:

class FakeApplicationTests: XCTestCase {
    func test_openUrl_success() {
        // Arrange
        let app = FakeApplication()
        let sut = AppLinker(application: app)
        guard let url = URL(string: "https://google.com") else {
            XCTFail("URL is wrong"); return
        }
        // Act
        sut.open(url: url)
        // Assert
        XCTAssertEqual(app.lastOpenedUrl, url)
    }
}

The “AppLinker” class has a dependency “Application”, that opens URL.

This dependency is also defined as a protocol that can have multiple implementations:

protocol Application: NSObject {
    @discardableResult
    func openURL(_ url: URL) -> Bool
}

We can easily give the whatever object we want in the initialiser, in this way:

class AppLinker {
    private let application: Application
   
    init(application: Application) {
        self.application = application
    }
   
    func open(url: URL) {
        application.openURL(url)
    }
}

Here we have extended UIApplication with this protocol, utilizing it as a dependency:

extension UIApplication: Application {}

The “FakeApplication” will be injected in the “SUT” AppLinker object:

class FakeApplication: NSObject, Application {
    var lastOpenedUrl: URL?
   
    func openURL(_ url: URL) -> Bool {
        self.lastOpenedUrl = url
        return true
    }
}

System under test (SUT) - refers to a system that is being tested for correct operation.

By the way you can read more about dependency injection here.

Test behavior, not implementation

Tests should verify behavior (what the code does), not implementation (how does it do it).

When we focus on testing the behavior we achieve two goals:

  • tests can be read as documentation of what each unit is supposed to do; 
  • testing the behavior allows us to change the implementation and refactoring.

When you have tests for the behavior of the class, you may change any line of code there and know if you broke something.

Conclusion

As shown Unit Testing could significantly simplify and impact the development process of your solution. The previously mentioned Unit Testing practices, if correctly applied, can prevent project time and cost overruns as they help to identify bugs at the programming stage. 

Unit Testing has long been a major part of Axon best practices. Our software engineers have acquired a foundational understanding of the automated testing principles and utilize them to develop custom software solutions for our clients. If you are ready to take the quality of your product to the next level, leave your contact information and our manager will reply as quickly as possible. 

In our next article from the custom software development series we are talking about localization management in applications and how it could be simplified. Learn more!

Axon Development Group
January 14, 2021
Software Development

readers who are obsessed with delivering great customer service.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Expertly curated emails that’ll help you deliver an exceptional customer experience.

Contact with us

Upload file with the file dialog or by dragging and dropping onto the dashed region

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.