Integrating EarlGrey 2.0 with Tuist

This guide will walk you through integrating EarlGrey 2 UI testing framework with your iOS project using Tuist. EarlGrey 2 is Google’s native iOS UI automation test framework that enables you to write reliable UI tests.

You’ll need your companion to this guide: my earlgrey-tuist repo.

Architecture Overview

EarlGrey 2 uses a unique architecture for UI testing that differs from traditional XCTest UI testing. Here’s how the components work together:

  1. Your App: Your existing iOS application that you want to test.
  2. UITestRig: A special version of your app that includes EarlGrey’s AppFramework, enabling it to communicate with the test code.
  3. UITests: The actual test bundle containing your EarlGrey test code.
  4. OpenBoxUtils: A helper bundle that enables “open box” testing, allowing direct access to your app’s internals during testing.

The architecture follows EarlGrey 2’s “open box” testing approach, which provides several advantages:

  • Direct access to application internals
  • More reliable element synchronization
  • Better control over the application state
  • Ability to modify and inspect the application’s runtime behavior

Prerequisites

  • Xcode 14.0 or later
  • Tuist installed
  • iOS project set up with Tuist
  • Basic understanding of UI testing

Setup Steps

1. Add EarlGrey as a Git Submodule

First, add EarlGrey as a git submodule to your project:

git submodule add https://github.com/google/EarlGrey.git Vendor/EarlGrey
git submodule update --init --recursive

2. Configure Project.swift

The Project.swift configuration requires several targets that work together to enable EarlGrey testing. Here’s a detailed explanation of each target:

let project = Project(
    name: "YourApp",
    organizationName: "Your Organization",
    targets: [
        // Your main application target
        // This is your existing app target and isn't directly related to EarlGrey
        // It's included here because the UITestRig target needs to mirror its setup
        .target(
            name: "App",
            destinations: .iOS,
            product: .app,
            bundleId: "your.bundle.id",
            infoPlist: .default,
            sources: ["App/Sources/**"],
            headers: .headers(project: ["App/Sources/**"])
        ),

        // OpenBoxUtils Bundle Target
        // This is a special bundle that enables "open box" testing capabilities
        // It loads into your test application and provides direct access to internals
        // The bundle is built for macOS but runs on iOS through the bundleLoader setting
        .target(
            name: "OpenBoxUtils",
            destinations: .macOS,
            product: .bundle,
            bundleId: "your.bundle.id",
            sources: ["OpenBoxUtils/Sources/**"],
            headers: .headers(private: ["OpenBoxUtils/Sources/**.h"]),
            dependencies: [
                .project(target: "AppFramework", path: "Vendor/EarlGrey", status: .optional),
                .target(name: "UITestsRig"),
            ],
            settings: .settings(
                base: SettingsDictionary()
                    .bitcodeEnabled(false)
                    .bundleLoader("UITestsRig")
                    .otherLinkerFlags(["-ObjC"])
                    .sdkRoot("iphoneos")
                    .userHeaderSearchPaths([
                        "Vendor/EarlGrey/**",
                        "Vendor/EarlGrey/Submodules/eDistantObject/**",
                    ])
            )
        ),

        // UI Tests Rig Target
        // This is a copy of your main app that includes EarlGrey's AppFramework
        // It serves as the test host and enables communication with test code
        .target(
            name: "UITestsRig",
            destinations: .iOS,
            product: .app,
            bundleId: "your.bundle.id",
            infoPlist: .default,
            sources: ["App/Sources/**"],
            headers: .headers(project: ["App/Sources/**"]),
            dependencies: [] // No direct EarlGrey dependencies needed here
        ),

        // UI Tests Target
        // This is where your actual EarlGrey test code lives
        .target(
            name: "UITests",
            destinations: .iOS,
            product: .uiTests,
            bundleId: "your.bundle.id",
            sources: ["App/UITests/**"],
            headers: .headers(private: ["App/UITests/**.h"]),
            scripts: [
                // Simplified copy scripts using Tuist's built-in helpers
                .copy(product: "OpenBoxUtils.bundle", destination: "UITestsRig.app", directory: "EarlGreyHelperBundles"),
                .copy(product: "AppFramework.framework", destination: "UITestsRig.app", directory: "Frameworks"),
            ],
            dependencies: [
                .project(target: "TestLib", path: "Vendor/EarlGrey"),
                .target(name: "OpenBoxUtils"),
                .target(name: "UITestsRig"),
            ],
            settings: .settings(
                base: SettingsDictionary()
                    .otherLinkerFlags(["-ObjC"])
                    .userHeaderSearchPaths([
                        "Vendor/EarlGrey/**",
                        "Vendor/EarlGrey/Submodules/eDistantObject/**",
                        "OpenBoxUtils/**",
                    ])
                    .merging(["BUNDLE_LOADER": "$(TEST_HOST)"])
            )
        ),
    ],
    // Define a scheme that includes the test target
    schemes: [.schemeWithTest(target: "UITestsRig", test: "UITests")]
)

Key changes in this configuration:

  1. Simplified Copy Scripts: Using Tuist’s built-in .copy helper instead of manual script phases, making the setup more reliable.
  2. Bundle Loading: The OpenBoxUtils bundle loader is now simplified to just reference “UITestsRig”.
  3. Header Search Paths: Added OpenBoxUtils to the header search paths in UITests target.
  4. Test Host Configuration: Added BUNDLE_LOADER setting to properly configure the test host.
  5. Scheme Definition: Explicitly defined a scheme that includes both the test host and test target.
  6. Dependencies: Removed direct AppFramework dependency from UITestsRig and added OpenBoxUtils dependency to UITests.

Important Note About Copy Scripts

EarlGrey 2.0’s official documentation recommends using Xcode’s “Copy Files” build phase to copy the required frameworks and bundles. The recommended setup looks like this:

copyFiles: [
    .absolutePath(
        name: "Copy AppFramework.framework",
        subpath: "$(TARGET_BUILD_DIR)/$(TEST_HOST)/Frameworks",
        files: [
            .folderReference(path: "AppFramework.framework"),
        ]
    ),
    .absolutePath(
        name: "Copy OpenBoxUtils.bundle",
        subpath: "$(TARGET_BUILD_DIR)/$(TEST_HOST)/EarlGreyHelperBundles",
        files: [
            .folderReference(path: "OpenBoxUtils.bundle"),
        ]
    ),
]

However, Tuist currently doesn’t support the exact copyFiles configuration that EarlGrey requires, specifically:

  • It doesn’t support copying to absolute paths with build variables
  • It doesn’t handle folder references in the way EarlGrey expects
  • The timing of when files are copied might differ from Xcode’s native build phases

To work around these limitations, we use Tuist’s .copy helper, which provides a simpler but equally effective solution:

scripts: [
    .copy(product: "OpenBoxUtils.bundle", destination: "UITestsRig.app", directory: "EarlGreyHelperBundles"),
    .copy(product: "AppFramework.framework", destination: "UITestsRig.app", directory: "Frameworks"),
],

This approach:

  • Copies the files to the correct locations relative to the test host application
  • Ensures proper timing of the copy operations
  • Maintains the same end result as EarlGrey’s recommended setup
  • Is more maintainable within Tuist’s project structure

The key differences are:

  1. Uses relative paths instead of absolute paths with build variables
  2. Copies products directly rather than folder references
  3. Simplifies the configuration while maintaining functionality
  4. Works reliably within Tuist’s build system

3. EarlGrey Project Configuration

EarlGrey itself is configured through its own Project.swift in the Vendor/EarlGrey directory. The configuration has been simplified to include only the essential targets:

  • TestLib: A static library that contains all the test-side functionality, including:

    • EarlGrey shorthand methods
    • Common library code
    • eDistantObject components (Channel, Device, Measure, Service)
    • Test-specific functionality
  • AppFramework: The main framework that gets embedded in your test application, containing:

    • App-side EarlGrey functionality
    • Common library code
    • UI interaction code
    • eDistantObject components
    • Third-party dependencies

The key aspects of this configuration are:

  1. TestLib Configuration:

    .target(
        name: "TestLib",
        destinations: .iOS,
        product: .staticLibrary,
        bundleId: "com.google.earlgrey.TestLib",
        sources: [
            "EarlGrey/AppFramework/**/*Shorthand.m",
            "EarlGrey/CommonLib/**/*.m",
            "EarlGrey/Submodules/eDistantObject/**/*.m",
            "EarlGrey/TestLib/**/*.m",
        ],
        settings: .settings(
            base: .targetBase()
                .headerSearchPaths([
                    "EarlGrey/AppFramework/**",
                    "EarlGrey/CommonLib/**",
                    "EarlGrey/UILib/**",
                    "EarlGrey/TestLib/**",
                    "EarlGrey/Submodules/eDistantObject/**"
                ])
                .otherLinkerFlags(["-ObjC", "-framework XCTest"])
        )
    )
    
  2. AppFramework Configuration:

    .target(
        name: "AppFramework",
        destinations: .iOS,
        product: .framework,
        bundleId: "com.google.earlgrey.AppFramework",
        sources: [
            "EarlGrey/AppFramework/**/*.m",
            "EarlGrey/CommonLib/**/*.m",
            "EarlGrey/UILib/**/*.m",
            "EarlGrey/Submodules/eDistantObject/**/*.m",
            "EarlGrey/third_party/**/*.c",
        ],
        dependencies: [.sdk(name: "IOKit", type: .framework)],
        settings: .settings(
            base: .targetBase()
                .headerSearchPaths([
                    "EarlGrey/",
                    "EarlGrey/CommonLib/**",
                    "EarlGrey/Submodules/eDistantObject/**",
                    "EarlGrey/UILib/**",
                ])
        )
    )
    

The configuration is now more streamlined, with:

  • Consolidated source files and header paths
  • Simplified dependencies
  • Essential build settings only
  • Proper module and versioning configuration

You don’t need to modify this configuration as it’s already optimized for use with your Tuist project.

4. Create Directory Structure

Create the following directory structure in your project:

YourProject/
├── App/
│   ├── Sources/
│   └── UITests/
├── OpenBoxUtils/
│   └── Sources/
└── Vendor/
    └── EarlGrey/

5. Writing UI Tests

Here’s how to write UI tests using EarlGrey 2. Create a new test file in App/UITests/:

// FunctionalTest.h
#import <XCTest/XCTest.h>

@interface FunctionalTest : XCTestCase
@end

// FunctionalTest.m
#import "FunctionalTest.h"
#import <EarlGrey/EarlGrey.h>

@implementation FunctionalTest

- (void)testBasicInteraction {
    // Launch the application
    XCUIApplication *application = [[XCUIApplication alloc] init];
    [application launch];

    // Example: Verify a button exists and tap it
    [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"MyButton")]
        performAction:grey_tap()];

    // Example: Verify text in a label
    [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"ResultLabel")]
        assertWithMatcher:grey_text(@"Expected Text")];
}

@end

6. Building and Running Tests

  1. Generate the Xcode project:

    tuist generate
    
  2. Run the UI tests:

    tuist test UITestsRig -- -destination "platform=iOS Simulator,name=iPhone 14"
    

Note: The tuist test command will automatically build all necessary dependencies, including the main app, before running the tests. You don’t need to build the app separately.

Common EarlGrey Matchers and Actions

Here are some commonly used EarlGrey matchers and actions:

// Matchers
grey_accessibilityLabel(@"Label")  // Find by accessibility label
grey_text(@"Text")                 // Find by text
grey_buttonTitle(@"Title")         // Find button by title
grey_kindOfClass([UIButton class]) // Find by class type

// Actions
grey_tap()                         // Tap element
grey_typeText(@"Hello")           // Type text
grey_scrollToContentEdge(kGREYContentEdgeRight) // Scroll
grey_swipeFastInDirection(kGREYDirectionLeft)   // Swipe

// Assertions
assertWithMatcher:grey_notNil()    // Assert element exists
assertWithMatcher:grey_enabled()   // Assert element is enabled
assertWithMatcher:grey_visible()   // Assert element is visible

Best Practices

  1. Accessibility Labels: Always set meaningful accessibility labels on UI elements you want to test:

    button.accessibilityLabel = "LoginButton"
    
  2. Test Structure:

    • Keep tests focused and atomic
    • Use setup and teardown methods for common initialization
    • Follow the Arrange-Act-Assert pattern
  3. Synchronization:

    • EarlGrey handles most synchronization automatically
    • Use GREYCondition for custom wait conditions
    • Avoid arbitrary delays
  4. Error Handling:

    • Add proper error handling and assertions
    • Use meaningful test failure messages

Troubleshooting

Common issues and solutions:

  1. Build Errors:

    • Ensure all required frameworks are properly linked
    • Check that search paths are correctly set in Project.swift
    • Verify EarlGrey submodule is properly initialized
  2. Test Failures:

    • Check if elements have correct accessibility labels
    • Verify UI elements are visible and enabled
    • Ensure proper synchronization between actions
  3. Framework Not Found:

    • Verify the copy script phases are executing correctly
    • Check that framework search paths are properly set

Additional Resources

Contributing

If you find issues or have improvements to suggest, please feel free to create an issue or submit a pull request to my earlgrey-tuist repo.

Comments

No comments yet. Be the first to comment!

Leave a Comment