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?

Frederik Winkelsdorf
  • 4,383
  • 1
  • 34
  • 42
netshark1000
  • 7,245
  • 9
  • 59
  • 116

12 Answers12

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.

Joe Masilotti
  • 16,815
  • 6
  • 77
  • 87
  • 3
    I don't want to lock my app to one language because I would like to create localized screenshots with https://github.com/fastlane/snapshot – netshark1000 Nov 09 '15 at 13:41
  • 5
    Please see my edit for a second option to use `accessibilityIdentifier`. – Joe Masilotti Nov 09 '15 at 17:44
  • Thanks this is the answer i've been searching for! works great! multi language UITesting and taking screenshots with fastlane – Gerrit Post Feb 05 '16 at 07:32
  • @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. – Bruno Muniz Sep 21 '17 at 09:11
  • 1
    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. – Ash Mar 28 '18 at 17:11
  • This gets a real problem when working with system buttons such as UIBarButtonItem - You cannot set it's accessibilityIdentifier =[ APPLE - DO SOMETHING – Yitzchak May 09 '18 at 18:38
26

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.

Community
  • 1
  • 1
SeanR
  • 7,899
  • 6
  • 27
  • 38
  • 2
    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? – Thermometer Sep 06 '17 at 09:45
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: http://stackoverflow.com/questions/33086266/cant-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

Volodymyr Prysiazhniuk
  • 1,897
  • 4
  • 22
  • 33
  • 1
    tried this solution, but not worked because **deviceLanguage** is `en-US`, but resource is `en`. Changed to `Locale(identifier: deviceLanguage).languageCode` – Kirow Jun 24 '19 at 14:16
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.

Vladimir Shutyuk
  • 2,956
  • 1
  • 24
  • 26
  • 5
    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 names – keeshux Apr 05 '16 at 17:58
  • @keeshux 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. – Vladimir Shutyuk Apr 16 '16 at 16:04
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
}
matsoftware
  • 766
  • 6
  • 12
  • 2
    Thanks. It worked for me. It's worth to mention that the variable _deviceLanguage_ is static and defined in **SnapshotHelper##setupSnapshot(_)**. – Oleksandr Nov 04 '16 at 12:54
  • 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).languageCode – randomcontrol Jul 08 '19 at 13:51
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];
}
Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
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.

J. Doe
  • 12,159
  • 9
  • 60
  • 114
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.

Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
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 "?"
}
Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
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

Taku
  • 5,639
  • 2
  • 42
  • 31
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;
}
Skander Fathallah
  • 503
  • 1
  • 5
  • 10
-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 ""
    }

}
Nadzeya
  • 641
  • 6
  • 16
  • If someone knows why this is not helpful, please let me know in comment. Would be happy to check it again and correct. – Nadzeya Nov 23 '20 at 14:03