Подтвердить что ты не робот

Должен ли NSUserDefault быть чистым сланцем для модульных тестов?

Я пишу свои первые iOS-тесты (Xcode 5, iOS 6) и обнаружил, что результаты модульных тестов варьируются в зависимости от того, что я сделал в Simulator в последнее время. Например. Я нажимаю на пользователя в списке контактов в Симуляторе, и теперь мои данные "последних контактов" в UserDefaults имеют еще один объект, чем раньше, даже когда я выполняю модульные тесты.

Для модульного тестирования он не чист, чтобы иметь случайные данные по умолчанию пользователя (я использую тесты RoR с их собственным чистым db). Кроме того, я могу протестировать определенные состояния, например, с пустыми данными "последних контактов".

От взгляда на связанные вопросы здесь, я представляю некоторые возможные ответы, которые мне не нравятся.

  • Mock UserDefaults для модульных тестов! Мне пришлось бы модифицировать многие существующие классы, чтобы я мог ввести этот макет.
  • Очистить или настроить UserDefaults в методе setUp! Но тогда мои данные, созданные с трудом в ручном тестировании, исчезнут.
  • Очистить или настроить UserDefaults в методе setUp , а затем восстановить эти значения в tearDown! Уч.

Они кажутся излишне сложными для чего-то, что должно быть стандартной практикой в ​​модульных тестах. Я не хочу повторять себя в каждом unit test. Итак, мои вопросы:

  • Не хватает ли чего-то желательного в том, как сохраняются UserDefaults из ad-hoc-симулятора, проверяющего до unit test?
  • Есть ли настраиваемый способ исправить это, скажем, каким-то образом установить цель unit test иметь другое хранилище для UserDefaults, чем когда я использую Simulator для проверки вручную?
  • В противном случае, есть ли элегантный способ сделать это в коде?
  • Например, я мог бы наследовать объект MyAppTestCase от XCTestCase и переопределять методы setUp и tearDown, которые нужно всегда отложить, а затем восстановить UserDefaults. Это хорошая идея?
4b9b3361

Ответ 1

Использование названных наборов как в этом ответе, работало хорошо для меня. Удаление пользовательских значений по умолчанию, используемых для тестирования, также можно выполнить в func tearDown().

class MyTest : XCTestCase {
    var userDefaults: UserDefaults?
    let userDefaultsSuiteName = "TestDefaults"

    override func setUp() {
        super.setUp()
        UserDefaults().removePersistentDomain(forName: userDefaultsSuiteName)
        userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
    }
}

Ответ 2

Доступный iOS 7/10.9

Вместо того, чтобы использовать стандартныеUserDefaults, вы можете использовать имя набора для загрузки тестов

[[NSUserDefaults alloc] initWithSuiteName:@"SomeOtherTests"];

Это в сочетании с некоторым кодом для удаления файла SomeOtherTests.plist из соответствующего каталога в setUp будет архивировать желаемый результат.

Вам придется создавать любые объекты, чтобы принимать объекты по умолчанию, чтобы не было никаких побочных эффектов от тестов.

Ответ 3

Как показывает @Tell, ваш дизайн, вероятно, неверен для хорошей проверки. Вместо того, чтобы иметь единичные элементы системы, читайте NSUserDefaults напрямую, они должны работать с каким-то другим объектом (который может разговаривать с NSUserDefaults). Это примерно эквивалентно "насмешкам NSUserDefaults", но на самом деле это дополнительный уровень абстракции. Ваш объект конфигурации будет абстрагировать как NSUserDefaults, так и другое хранилище конфигурации, такое как keychain. Это также гарантирует, что вы не разбросаете строковые константы вокруг программы. Я создал такой тип конфигурации для многих проектов и настоятельно рекомендую его.

Некоторые утверждают, что объекты, подлежащие тестированию, не должны опираться на такие синглтоны, как NSUserDefaults или мой глобальный объект конфигурации вообще. Вместо этого вся конфигурация должна быть введена в init. На практике я нахожу, что это создает слишком большую головную боль при взаимодействии с Storyboards, но это стоит учитывать в тех местах, где это может быть полезно.

Если вы действительно хотите глубоко погрузиться в NSUserDefaults, это обеспечивает некоторую возможность расслоения. Вы можете исследовать setVolatileDomain:forName:, чтобы увидеть, можете ли вы создать дополнительный слой для своего unit test. На практике мне не очень повезло с такими вещами на iOS (более того, на Mac, но все равно не до уровня, которому вам нужно доверять).

Можно swizzle standardUserDefaults, но я бы не рекомендовал этот подход, если вы можете его избежать. Ваш "сохранить все в начале и восстановить все в конце" - это, пожалуй, лучший стандартизованный способ подойти к проблеме, если вы не можете адаптировать свой дизайн, чтобы избежать внешних эффектов.

Ответ 4

Вы можете легко сохранить и восстановить постоянный домен для основного идентификатора пакета, для которого записывается [[NSUserDefaults standardUserDefaults] setObject:forKey:]. Например,

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *originalValues = [defaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]];

// do stuff, possibly [defaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]
// or using setPersistentDomain: to substitute a dictionary of mock values and test against that

[defaults setPersistentDomain:originalValues forName:[[NSBundle mainBundle] bundleIdentifier]];

Вы также можете использовать [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain], если вы хотите получить доступ к одному объединенному словарю того материала, который вы регистрируете, используя все вызовы -registerDefaults: (по крайней мере, для любого кода, который был запущен до того, где был запущен unit test конечно).

Ответ 5

Мне нравится создавать новый, чтобы не было сговора.

import XCTest

extension UserDefaults {
    private static var index = 0
    static func createCleanForTest(label: StaticString = #file) -> UserDefaults {
        index += 1
        let suiteName = "UnitTest-UserDefaults-\(label)-\(index)"
        UserDefaults().removePersistentDomain(forName: suiteName)
        return UserDefaults(suiteName: suiteName)!
    }
}

class MyTest: XCTestCase {

    func testOne() {
        let userDefaults = UserDefaults.createCleanForTest()
        XCTAssertFalse(userDefaults.bool(forKey: "foo"))
        userDefaults.set(true, forKey: "foo")
        XCTAssertTrue(userDefaults.bool(forKey: "foo"))
    }

    func testTwo() {
        let userDefaults = UserDefaults.createCleanForTest()
        XCTAssertFalse(userDefaults.bool(forKey: "foo"))
        userDefaults.set(true, forKey: "foo")
        XCTAssertTrue(userDefaults.bool(forKey: "foo"))
    }
}