crowdin/mobile-sdk-ios

End to end tests for important use cases

chrispomeroyhale opened this issue · 3 comments

Is your feature request related to a problem? Please describe.
My team is exploring integrating with the Crowdin SDK which we are excited about. I have been tasked with finding out what happens if we depend on Over-the-Air translations and there's a network failure.

I started writing some additional tests. It became clear that although some aspects of this project are easily testable, the changes required to make LocalizationProvider and all of its dependencies testable is a big lift and I am unfamiliar with this code base.

Describe the solution you'd like
It would be great to see some expansion of test cases to cover business critical functions to give us more confidence about the SDK. For instance, in at least one place a callback is missing -- does this have any real world consequence? Are there any other cases like this?

Some of the SDK is already using protocols which is a great! But some parts are using global state which makes it hard to test.

LocalizationProvider can already be passed in a CrowdinRemoteLocalizationStorage but we are currently unable to pass in a CrowdinLocalizationDownloader in order to use the existing URLSessionMock to simulate successful and failed network respones. This may also require creating a mock for CrowdinContentDeliveryAPI to pass in a manifest. Additionally, although CrowdinContentDeliveryAPI has some test coverage, it does not cover the downloading of the manifest.

Describe alternatives you've considered
I successfully added a mock for RemoteLocalizationStorageProtocol and passed that into CrowdinSDK.startWithRemoteStorage(). However, this doesn't give deep enough test coverage into what happens during a network failure.

Additional context
I may have some time next week to work with you towards a solution.

Hi @chrispomeroyhale !
Thank you for sharing all the details with us, our developers will be able to check everything in detail and answer you from Monday
In the meantime, I wish you a good day!🙂

I'd like to share what I have figured out so far, which the developers are free to use or modify or reject.

My first attempt tries to see what happens if the RemoteLocalizationStorageProtocol doesn't return any strings -- simulating what might happen if the network requests fail. The code below shows that the Localization falls back to the translation from the Localizable.strings bundled with the test target.

The pitfall is we aren't actually testing whether the CrowdinRemoteLocalizationStorage functions correctly in various use cases. I found that testing CrowdinRemoteLocalizationStorage is challenging because the code isn't currently set up to pass URLSessionMock, and there is some global state with the Manifest that makes this complicated.

    func testRemoteLocalizationDoesntExist() {
        let started = expectation(description: "SDK started")
        let remoteStorage = RemoteLocalizationStorageMock()
        CrowdinSDK.startWithRemoteStorage(remoteStorage) {
            started.fulfill()
        }
        wait(for: [started], timeout: 60)
        XCTAssertEqual(CrowdinSDK.inBundleLocalizations.count, 3)
        XCTAssertEqual(Localization.current.localizedString(for: "test_key"), "test_value [B]")
        CrowdinSDK.deintegrate()
        CrowdinSDK.stop()
    }

    func testRemoteLocalizationExists() {
        let started = expectation(description: "SDK started")
        let remoteStorage = RemoteLocalizationStorageMock(strings: ["test_key": "test_value [testRemoteLocalizationExists]"])
        CrowdinSDK.startWithRemoteStorage(remoteStorage) {
            started.fulfill()
        }
        wait(for: [started], timeout: 60)
        XCTAssertEqual(CrowdinSDK.inBundleLocalizations.count, 3)
        XCTAssertEqual(Localization.current.localizedString(for: "test_key"), "test_value [testRemoteLocalizationExists]")
        CrowdinSDK.deintegrate()
        CrowdinSDK.stop()
    }
class RemoteLocalizationStorageMock: RemoteLocalizationStorageProtocol {

    let strings: [String: String]
    let plurals: [AnyHashable: Any]

    init(name: String = "Mock", strings: [String: String] = [:], plurals: [AnyHashable: Any] = [:]) {
        self.name = name
        self.strings = strings
        self.plurals = plurals
    }

    // MARK: RemoteLocalizationStorageProtocol

    var name: String

    func prepare(with completion: @escaping () -> Void) {
        completion()
    }

    var localizations: [String] = ["en"]

    var localization: String = "en"

    func deintegrate() {}

    func fetchData(completion: @escaping LocalizationStorageCompletion, errorHandler: LocalizationStorageError?) {
        completion(localizations, localization, strings, plurals)
    }

}

Hi!
Thanks for the additional comment, will share it with the team
Once we have any news - we'll come back with the reply!