Должен ли 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. Это хорошая идея?
Ответы
Ответ 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"))
}
}