Как выполнить Unit-Test с глобальными структурами? - PullRequest
2 голосов
/ 08 марта 2019

Для меня очевидно, что в UnitTest вы

  1. генерируете входное свойство
  2. передаваете это свойство методу, который вы хотите проверить
  3. Сравнитерезультаты с вашими ожидаемыми результатами

Однако что делать, если у вас есть глобальная структура, например, с игровым xp и игровым уровнем, которая имеет частные сеттеры и не может быть изменена.Я автоматически загружаю эти данные из UserDefaults при запуске приложения.Как вы можете тестировать методы, которые обращаются к этой глобальной структуре, когда вы не можете изменить ввод?

Пример:

import UIKit

//Global struct with private data
struct GameStatus {
    private(set) static var xp: Int = 0
    private(set) static var level: Int = 0

    /// Holds all winning states
    enum MyGameStatus {
        case hasNotYetWon
        case hasWon
    }

    /// Today's game state of the user against ISH
    static var todaysGameStatus: MyGameStatus {
        if xp >= 100 {
            return .hasWon
        } else {
            return .hasNotYetWon
        }
    }

    func restoreXpAndLevel() {
        // reads UserData value
    }

    func increaseXp(for: Int) {
        //...
    }
}

// class with methods to test
class LevelView: UIView {

    enum LevelState {
        case showStart
        case showCountdown
        case showFinalCuontdown
    }

    var state: LevelState {
        if GameStatus.xp > 95 {
            return .showFinalCuontdown
        } else if GameStatus.xp > 90 {
            return .showCountdown
        }
        return .showStart
    }

    //...configurations depending on the level
}

Ответы [ 2 ]

2 голосов
/ 08 марта 2019

Во-первых, LevelView выглядит так, как будто в нем слишком много логики. Точка зрения заключается в отображении данных модели. Это не включает бизнес-логику, такую ​​как GameStatus.xp > 95. Это должно быть сделано в другом месте и установлено в поле зрения.

Далее, почему GameStatus статичен? Это только усложняет это. Передайте GameStatus представлению, когда оно изменится. Это работа контроллера представления. Взгляды просто рисуют вещи. Если на ваш взгляд что-то действительно может быть проверено юнитами, то, вероятно, этого не должно быть.

Наконец, часть, с которой вы боретесь - это пользовательские настройки по умолчанию. Так что извлеките этот кусок в общий GameStorage.

protocol GameStorage {
    var xp: Int { get set }
    var level: Int { get set }
}

Теперь сделайте UserDefaults в GameStorage:

extension UserDefaults: GameStorage {
    var xp: Int {
        get { /* Read from UserDefaults */ return ... }
        set {  /* Write to UserDefaults */ }
    }
    var level: Int {
        get { /* Read from UserDefaults */ return ... }
        set {  /* Write to UserDefaults */ }
    }
}

А для тестирования создайте статический:

struct StaticGameStorage: GameStorage {
    var xp: Int
    var level: Int
}

Теперь, когда вы создаете GameStatus, передайте ему хранилище. Но вы можете присвоить этому значению значение по умолчанию, чтобы вам не приходилось передавать его все время

class GameStatus {
    private var storage: GameStorage

    // A default parameter means you don't have to pass it normally, but you can
    init(storage: GameStorage = UserDefaults.standard) {
        self.storage = storage
    }

При этом xp и level могут просто передаваться в хранилище. Нет необходимости в специальном шаге «загрузить хранилище сейчас».

private(set) var xp: Int {
    get { return storage.xp }
    set { storage.xp = newValue }
}
private(set) var level: Int {
    get { return storage.level }
    set { storage.level = newValue }
}

РЕДАКТИРОВАТЬ: я сделал здесь изменение от GameStatus в качестве структуры к классу. Это потому, что GameStatus не хватает семантики значения. Если существует две копии GameStatus, и вы изменяете одну из них, другая также может измениться (поскольку они обе записывают в UserDefaults). Структура без семантики значения опасна.

Возможно восстановить семантику значения, и это стоит рассмотреть. Например, вместо того, чтобы проходить через xp и level в хранилище, вы можете вернуться к исходному дизайну, который имеет явный шаг «восстановления», который загружается из хранилища (и я предполагаю, что шаг «сохранения» записывает в хранилище). Тогда GameStatus будет подходящей структурой.


Я также извлек бы LevelState, чтобы вам было проще его протестировать, и он захватывает бизнес-логику за пределами представления.

enum LevelState {
    case showStart
    case showCountdown
    case showFinalCountDown
    init(xp: Int) {
        if xp > 95 {
            self = .showFinalCountDown
        } else if xp > 90 {
            self = .showCountdown
        }
        self = .showStart
    }
}

Если это только когда-либо используется этим одним представлением, это хорошо, чтобы вкладывать его. Только не делай это частным. Вы можете протестировать LevelView.LevelState без каких-либо действий с самим LevelView.

И затем вы можете обновить GameStatus представления так, как вам нужно:

class LevelView: UIView {

    var gameStatus: GameStatus? {
        didSet {
            // Refresh the view with the new status
        }
    }

    var state: LevelState {
        guard let xp = gameStatus?.xp else { return .showStart }
        return LevelState(xp: xp)
    }

    //...configurations depending on the level
}

Теперь само представление не нуждается в логическом тестировании. Вы можете провести тестирование на основе изображений, чтобы убедиться, что оно правильно рисует при различных входных данных, но это полностью сквозное. Вся логика проста и тестируема. Вы можете протестировать GameStatus и LevelState вообще без UIKit, передав StaticGameStorage в GameStatus.

1 голос
/ 08 марта 2019

Решением является внедрение зависимостей!

Вы можете создать протокол Persisting и класс фасада для взаимодействия с пользовательскими настройками по умолчанию

protocol Persisting {
  func getObject(key: String) -> Any?
  func persist(value: Any, key: String)
}

final class Persist: Persisting {
  func getObject(key: String) -> Any? {
    return UserDefaults.standard.object(forKey: key)
  }

  func persist(object: Any, key: String) {
    UserDefaults.standard.set(value: object, forKey: key)
  }
}

class MockPersist: Persisting {
  // this is set from the test
  var mockObjectToReturn: Any?
  func getObject(key: String) -> Any? {
    return mockObjectToReturn
  }

  var didCallPersistObject: (Any?, String)
  func persist(object: Any, key: String) {
    didCallPersistObject.0 = object
    didCallPersistObject.1 = key
  }
}

И теперь вы будете структурироватьсянужно ввести это переменную типа Persisting.

. При тестировании вам нужно будет ввести MockPersist и применить против переменных, определенных в классе MockPersist.

Надеюсь, что этопомогает

...