Swift’s New Testing Framework
WWDC 2024 unveiled a major improvement to XCTest, which is Swift’s Testing Framework. I found it really beneficial while working on this. So I decided to share it with all of you. Let’s begin.
Introduction
Swift Testing allows you to use the Swift programming language’s powerful and expressive capabilities to write tests with greater confidence and less code. The library works smoothly with Swift Package Manager and Xcode’s testing workflow, allowing for flexible test organization, customizable metadata, and scalable test execution
XCTest Vs Swift Testing
Now it’s time to figure out why we require the Swift Testing Framework when we already have the XCTest Framework. How does it vary from XCTest, and what are the advantages of utilizing it over XCTest?
XCTest Framework
XCTest is Apple’s default testing framework for Swift and Objective-C. It is built into Xcode and offers full support for unit, performance, and UI testing. XCTest is widely used because of its smooth interaction with Xcode, making it the preferred choice for many Swift developers.
Key Features
- Integration with Xcode: XCTest is strongly linked with Xcode, allowing for the simple creation of tests, execution, and debugging.
- Assertion Library: XCTest provides a variety of assertions to validate test scenarios. Which makes it simple to utilize.
- Code Coverage: Supports code coverage analysis to discover untested code.
Example:
Swift Testing Framework
Swift Testing Framework, also known as Swift Test, is a modern framework created exclusively for writing tests in Swift. It seeks to give a more Swift-like, expressive, and powerful method of writing tests than the existing XCTest framework.
Key Features
- DSL for Tests: Swift Testing Frameworks frequently employ a Domain-Specific Language (DSL) to make test creation more intuitive and readable. This can result in tests that appear more like normal language than code.
- First-Class Swift Support: These frameworks, which are specifically created for Swift, take advantage of the language’s characteristics like optional, generics, and type inference to deliver a smoother testing experience.
- Mocking and Stubbing: Many modern Swift Testing Frameworks provide support for mocking and stubbing, making it easy to separate the code being tested.
- Asynchronous Testing: Improved support for testing asynchronous code, which is increasingly crucial in modern Swift applications.
Benefits of Swift Testing
- Early Bug Detection: Writing tests helps detect bugs and obstacles early in the development process, minimizing the cost and work required to repair them later.
- Refactoring Confidence: Tests serve as a safety net while rewriting code, ensuring that modifications do not introduce new bugs.
- Documentation: Tests serve as documentation, demonstrating how the code should react in different contexts.
- Continuous Integration: Automated tests can be added to continuous integration (CI) pipelines, guaranteeing that code changes are tested automatically.
Add Swift Testing
Let’s understand how to add Swift Testing to an Existing Project or a new one.
1. Adding In Existing Project:- To add a new target follow the steps.
Click File -> New -> Target
2. Choose Target:- In the target popup search for unit testing and select it as shown in the image.
3. Target Name:- Now give the name to the target and under the testing system select Swift Testing and click on finish.
Now we have imported Test Target here and It’s time to write our first swift Test case.
Building Block of Swift Testing
1. Add Test Case:
As per the above example, We just need to import the Testing framework to start working with Swift Testing. To make any function Testable we just need to add the @Test attribute. After adding this attribute a diamond box was added in front to test the function.
2. Adding Expectation
After adding the Test case we have to put our expectations by using macros provided by the framework.
i. #expect:- By using this macro we can define our expectations. It’s validated whether the given condition is true or false. We don’t need to add multiple asserts to validate Bool conditions like XCAssertEqual, XCTAssertNotEqual, XCTAssertTrue, etc. Let’s understand it by example.
As we can see one success expectation and one failure expectation. Here we are Testing two cases one is equalTo and the other is greaterThan and both can be executed using #expect macro. While in XCTest we have two asserts in this case.
ii: #expect(.throws: (any Error).self):- This macro is used to expect an error to be thrown. You specify the error type or the actual error that you expect to be thrown. The test will fail if the code within the macro does not throw an error, or if the error is not what is intended. By using (any Error).self, you are indicating that the code should throw any error that confirms the Error protocol.
iii. #require macro:- This macro is used, as its name suggests when a method needs to run certain prerequisites before continuing with the test case. If the necessary circumstances are not met, the test fails. Let us examine an example.
In the above example, we can see that if a name is nil #require macros get failed and stop the execution of #expect macro.
Swift’s New Testing Framework has added a number of new macros. In upcoming articles, we shall talk more about that. Let us now go to another set of building blocks.
3. Traits
Traits are basically to used for defining the characteristics, attributes, or behaviors of a block of code or function. It is also used to define the descriptive information of test cases. Let’s have a look at how to define a trait.
As we can see in the above example when we define the traits for any test case method it replaces the name of the function with the given traits in the Left Navigation Pane. It is useful to define the cases and easy to find out and debug when it fails.
There are so many Traits that are introduced with Swift’s New Testing Framework. You can check in this Link.
4. Test Suites
Suites are basically used to combine the Test function and Suites. It can be annotated using @suite. Any Test function is implicitly considered a suite. Suites can have stored properties. It can have init and deInit methods for set-up and tear-down logic respectively. Whenever we create an object of any suite it automatically calls init() method and after Test cases run successfully it needs to be de-initialized like we were working on class and structs. Let’s have a look at an example.
As we can see in the above example, All the test cases regarding Name are combined in a single suite and we have given descriptive info about the suite that will display in the left navigation pan as in the image.
Parameterized testing
Swift Testing framework allows us to pass parameters in a function that makes them testable with multiple different values. This makes tests more efficient and manageable. Let’s look an example.
We have created a test case that expects that the given email ID should not be nil. But at the same time, we got a compile time error which indicates that we have to pass the argument value within the @Test Attributes. Let’s correct it and see how it works.
As we can see in the given example, We are passing an array of strings and the Test function iterates each of them and validates them. It will display the result of each parameter whether that Passes or Fail and if fail then what will be the reason? We can also check the result in the Left Navigation Pan. We can re-execute only the failed argument.
Conditional Testing
While we are working big project that has several Test cases and is in maintenance mode, we get a task to change in existing behavior of the app. In this case, chances are very high to impact the test case. So We may be required to disable some test cases or only need to run in certain conditions. Here Swift Testing frameworks to help us to do this with minimal changes by using .enable or .disable attributes with @Test.
In the given example we have used .enable and .disable attributes. For using .enable attribute we need to pass a specific condition. If the condition is true it will execute else it will skip the test. Same with the disable attribute. If we add this attribute, the test case will skipped and show the description.
Conclusion
Test behaviors can be declared with less code thanks to Swift Testing’s expressive API with macros. Swift expressions and operators are used by the #expect API to validate the supplied expressions. Moreover, Swift Testing facilitates parallel testing, which raises the general productivity and efficiency of testing.