Xcode 7 UITests with localized UI
Asked Answered
C

12

42

In my App I'm using NSLocalizedString to localize my app. Now I want to switch to UITests and have Testcode like this:

[tabBarsQuery.buttons["particiants"] tap];

This works for English but fails for other languages.

[tabBarsQuery.buttons[NSLocalizedString("PARTICIPANTS",comment:nil)] tap];

Fails - probably because Localizable.strings is in another bundle. How can I test a localized app?

Caniff answered 9/11, 2015 at 12:40 Comment(0)
V
27

I wanted to actually test the content of UI features and not just their existence, so setting a default language or using the accessibility identifiers wouldn't suit.

This builds on Volodymyr's and matsoftware's answers. However their answers rely on deviceLanguage which needs to be explicitly set in SnapshotHelper. This solution dynamically gets the actual supported language the device is using.

  1. Add the Localizable.strings files to your UITest target.
  2. Add the following code to your UITest target:

    var currentLanguage: (langCode: String, localeCode: String)? {
        let currentLocale = Locale(identifier: Locale.preferredLanguages.first!)
        guard let langCode = currentLocale.languageCode else {
            return nil
        }
        var localeCode = langCode
        if let scriptCode = currentLocale.scriptCode {
            localeCode = "\(langCode)-\(scriptCode)"
        } else if let regionCode = currentLocale.regionCode {
            localeCode = "\(langCode)-\(regionCode)"
        }
        return (langCode, localeCode)
    }
    
    func localizedString(_ key: String) -> String {
        let testBundle = Bundle(for: /* a class in your test bundle */.self)
        if let currentLanguage = currentLanguage,
            let testBundlePath = testBundle.path(forResource: currentLanguage.localeCode, ofType: "lproj") ?? testBundle.path(forResource: currentLanguage.langCode, ofType: "lproj"),
            let localizedBundle = Bundle(path: testBundlePath)
        {
            return NSLocalizedString(key, bundle: localizedBundle, comment: "")
        }
        return "?"
    }
    
  3. Access the method by localizedString(key)

For those languages with a script code, the localeCode will be langCode-scriptCode (for example, zh-Hans). Otherwise the localeCode will be langCode-regionCode (for example, pt-BR). The testBundle first tries to resolve the lproj by localeCode, then falls back to just langCode.

If it still can't get the bundle, it returns "?" for the string, so it will fail any UI tests that look for specific strings.

Virtual answered 17/5, 2017 at 2:52 Comment(1)
This works great for my localizable.strings! However, I cannot seem to get it to work with my Storyboard string files. Do you have any idea how I can fix that? Doesn't the storyboard strings go to the eventual lproj folder when building?Prehensile
M
26

Option 1: Set a Default Language

Create a new scheme for UI Testing and set the default Application Language. This will lock the app into one localized file so you can write all of your tests for that language.

Set the option from Product -> Scheme -> Manage Schemes or ⌘⇧,. Then select the Options tab and set the language.

Xcode - Set the Default Application Language

Pros: Simple, one-time change.

Cons: Cannot be used to create localized screenshots with snapshot (a tool that runs your app via UI Testing and generates App Store screenshots along the way).

Option 2: Use -accessibilityIdentifier for Localized Strings

Instead of accessing items via their displayed text or value, use accessibilityIdentifier. This is read by the UI Testing framework but never shown or read to users (even with accessibility turned on). In the old UIAutomation docs Apple mentions using this for developer functionality, which this seams like a good use case.

You can then continue to set accessibilityLabel and accessibilityValue like normal, with the localized versions.

Pros: Can be used for more generic solutions, such as taking automated screenshots.

Cons: Might require more work changing each label you need "unlocalized" for testing.

Mate answered 9/11, 2015 at 13:30 Comment(6)
I don't want to lock my app to one language because I would like to create localized screenshots with github.com/fastlane/snapshotCaniff
Please see my edit for a second option to use accessibilityIdentifier.Mate
Thanks this is the answer i've been searching for! works great! multi language UITesting and taking screenshots with fastlaneVacationist
@JoeMasilotti I'm using KIF so i'm not using XCUI and therefore i can't use snapshot but i'm trying to find a way to set multiple languages because i want to start my simulator in each one of this languages to force my app run in each language - test in each language. Do you have any idea about a workaround? Thanks.Vday
How to address UIAlertsAction and UIAlertControllers as they dont respond to accessibility Ids. I tried with Accessibility Labels, they work fine in English but with other languages, its not working.Sweepback
This gets a real problem when working with system buttons such as UIBarButtonItem - You cannot set it's accessibilityIdentifier =[ APPLE - DO SOMETHINGA
N
17

YOU CAN RE-USE YOUR PROJECT LOCALIZATION BUNDLES!

When you test message boxes behaviour you need to know exactly what message box just appeared. You need to copy your localization from another scheme during build phase.

In your UI Tests target -> Build Phases -> Copy Bundle Resources, add the localization files needed (e.g. Localizable.strings).

Add a function similar to the following:

func localizedString(key:String) -> String {
/*1*/ let localizationBundle = NSBundle(path: NSBundle(forClass: /*2 UITestsClass*/.self).pathForResource(deviceLanguage, ofType: "lproj")!) 
/*3*/ let result = NSLocalizedString(key, bundle:localizationBundle!, comment: "") // 
    return result
}

/*1 Gets correct bundle for the localization file, see here: https://mcmap.net/q/391685/-can-39-t-get-access-to-string-localizations-in-ui-test-xcode-7 */
/*2 Replace this with a class from your UI Tests 
/*3 Gets the localized string from the bundle */

Then in your code you can use app.buttons[localizedString("localized.string.key")]

Full article is here: https://github.com/fastlane-old/snapshot/issues/321#issuecomment-159660882

Nubbly answered 30/6, 2016 at 21:56 Comment(1)
tried this solution, but not worked because deviceLanguage is en-US, but resource is en. Changed to Locale(identifier: deviceLanguage).languageCodeVadim
R
6

The simplest and reliable way for me so far is to reference elements with elementBoundByIndex() Like this:

    let app = XCUIApplication()
    let tabBar = app.tabBars
    tabBar.buttons.elementBoundByIndex(2).tap()
    app.navigationBars.buttons.elementBoundByIndex(0).tap()
    app.tables.cells.elementBoundByIndex(2).tap()
    app.tables.elementBoundByIndex(1).cells.elementBoundByIndex(0).tap()

You can guess/experiment with this values and find elements you need.

Rutherford answered 18/11, 2015 at 18:17 Comment(2)
rearrange layout later for whatever reason and you're screwed. tests will fail and go figure all the affected indexes each time. sorry but no, this is not even close to how reliable accessibilityIdentifier is. in order to make this feasible you should at the very least assign those elements to meaningful variable namesPromotive
@Promotive I agree :) That's why accepted answer recommends using accessibilityIdentifier. I needed this tests for fastlane/snapshot. And you make new screenshots when your "layout changes". And it is by far the fastest way for me.Rutherford
I
2

The answer of Volodymyr helped me a lot, but it can fail if the localization bundle folder name differs from the deviceLanguage set in Snapshot. This snippet works fine for me in Swift 3.0 and with languages like italian (where current locale is "it" but device language is "it-IT").

    func localizedString(key:String) -> String {
      let languageBundlePath = Bundle(for: PlinthUITests.self).path(forResource: deviceLanguage, ofType: "lproj") ?? Bundle(for: PlinthUITests.self).path(forResource: NSLocale.current.languageCode!, ofType: "lproj")
      let localizationBundle = Bundle(path: languageBundlePath!)
      let result = NSLocalizedString(key, bundle:localizationBundle!, comment: "")
    return result
}
Imminence answered 29/10, 2016 at 18:9 Comment(2)
Thanks. It worked for me. It's worth to mention that the variable deviceLanguage is static and defined in SnapshotHelper##setupSnapshot(_).Correna
Unfortunately NSLocale.current.languageCode in my case was "en" while deviceLanguage was "de-DE". But in a comment to Volodymyrs answer Kirow gave the missing hint: Locale(identifier: deviceLanguage).languageCodeEngel
B
1

In addition to Joe's answer you can also force language for UI tests directly in test code without editing a scheme like this:

- (void)setUp
{
    [super setUp];

    self.continueAfterFailure = NO;
    XCUIApplication *app = [[XCUIApplication alloc] init];
    app.launchArguments = @[@"-AppleLanguages", @"(en)", @"-AppleLocale", @"en_EN"];
    [app launch];
}
Bakemeier answered 26/6, 2017 at 14:39 Comment(0)
J
1

Different approach without calling methods on your String

Prerequisites

You are using NSLocalizedString.

Step 1

Make sure you add the translations to your test targets (Go to your Localizable file and on the right side you can tap your UI test targets).

Step 2

Add this somewhere in your main target

#if DEBUG
    // Can be changed by UI tests to get access to localized content
    var bundleForLocalizedTexts = Bundle.main
#else
    let bundleForLocalizedTexts = Bundle.main
#endif

Step 3

Add this value to the parameter bundle in all your NSLocalizedStrings, like this:

NSLocalizedString(
    "localized",
    bundle: bundleForLocalizedTexts,
    comment: ""
)

Step 4

Override the method setUp in your XCTestCase subclass and add this line:

bundleForLocalizedTexts = Bundle(for: MySubclass.self)

Step 5

Everything should work! All languages should work, no extra methods to call.

Jeunesse answered 19/1, 2023 at 22:17 Comment(0)
M
0

If you're doing this for the purpose of running Snapshot (rather than actual UI testing), then I find the simplest solution is to cheat and use HSTestingBackchannel

It is a tool that I wrote which allows you to send notifications from the UITesting class to the app. You then write code in the app which responds directly to the notifications.

Magnus answered 5/5, 2016 at 18:4 Comment(0)
F
0

The answer of SeanR is great (+1), but there is a minor improvement:

If you use base localization, then your Localizable.strings might not be localized in your base language. This is not necessary because the base language would be used in this case. If so, SeanR’s function localizedString would return „?“.

The extended version below checks additionally for the base language, and returns the localized string in the base language:

func localizedString(_ key: String) -> String {
    let testBundle = Bundle(for: ShopEasyUITests.self)
    guard let currentLanguage = currentLanguage else { return "?" }
    if let testBundlePath = testBundle.path(forResource: currentLanguage.localeCode, ofType: "lproj"),
        let localizedBundle = Bundle(path: testBundlePath) {
        return NSLocalizedString(key, bundle: localizedBundle, comment: "")
    }
    if let testBundlePath = testBundle.path(forResource: currentLanguage.langCode, ofType: "lproj"),
        let localizedBundle = Bundle(path: testBundlePath) {
        return NSLocalizedString(key, bundle: localizedBundle, comment: "")
    }
    if let testBundlePath = testBundle.path(forResource: "Base", ofType: "lproj"),
        let localizedBundle = Bundle(path: testBundlePath) {
        return NSLocalizedString(key, bundle: localizedBundle, comment: "")
    }
    return "?"
}
Farmhand answered 31/1, 2018 at 21:31 Comment(0)
M
0

For fastlane's snapshot feature, the SnapshotHelper.swift launches app with these arguments. So by interpreting these values, this solution is deterministic, and I was able to produce correct snapshots for multiple languages:

func getLocale(str: String) -> String {
    let start = str.index(str.startIndex, offsetBy: 1)
    let end = str.index(start, offsetBy: 2)
    let range = start..<end

    var locale = str.substring(with: range)
    if locale == "en" {
        return "Base"
    }
    return locale
}

func localizedString(_ key: String) -> String {
    print("app.launchArguments \(app.launchArguments)")
    guard let localeArgIdx = app.launchArguments.index(of: "-AppleLocale") else {
        return ""
    }
    if localeArgIdx >= app.launchArguments.count {
        return ""
    }
    let str = app.launchArguments[localeArgIdx + 1]
    let locale = getLocale(str: str)
    let testBundle = Bundle(for: Snapshot.self)
    if let testBundlePath = testBundle.path(forResource: locale, ofType: "lproj") ?? testBundle.path(forResource: locale, ofType: "lproj"),
        let localizedBundle = Bundle(path: testBundlePath)
    {
        return NSLocalizedString(key, bundle: localizedBundle, comment: "")
    }
    return ""
}

Hope this helps

Murdocca answered 16/4, 2018 at 12:32 Comment(0)
T
0

Objective-C Solution : inspired by @Volodymyr Prysiazhniuk solution

- (NSString*)getLocalizedStringForKey :(NSString*)stringKey forUITestClass : (id) uiTestClass{
    if (!stringKey || !uiTestClass){
        return nil;
    }
    NSString *bundlePath = [[NSBundle bundleForClass: uiTestClass]bundlePath];
    NSBundle* bundle = [NSBundle bundleWithPath:bundlePath];
    NSString* localizedString = NSLocalizedStringWithDefaultValue(stringKey, nil, bundle, nil, nil);
    return localizedString;
}
Townsend answered 8/1, 2019 at 15:30 Comment(0)
F
-1

I've made localized screenshots with Fastlane using next scheme:

1) Add to Fastfile next param:

localize_simulator: true

2) Use this code to get localized string:

class LocalizationHelper: NSObject {

    class func localizedString(_ key: String) -> String {
        let testBundle = Bundle(for: UITests.self)
        let currentLocale = Locale.current
        if let code = currentLocale.languageCode,
            let testBundlePath = testBundle.path(forResource: code, ofType: "lproj") ?? testBundle.path(forResource: code, ofType: "lproj"), let localizedBundle = Bundle(path: testBundlePath) {
            return NSLocalizedString(key, bundle: localizedBundle, comment: "")
        }
        return ""
    }

}
Ftc answered 21/5, 2020 at 21:32 Comment(1)
If someone knows why this is not helpful, please let me know in comment. Would be happy to check it again and correct.Ftc

© 2022 - 2024 — McMap. All rights reserved.