UserDefaultsを含むclassをテストするtips

# モチベーション
UserDefaultsを含むclassをテストする際に，アプリ本体のUserDefaultsに影響がない形でテストを書きたかった

# 環境
Xcode: Version 11.5 (11E608c)
Swift: 5
iOS: 13.5

# 今回のテスト対象のclass
**class名はViewModelですが，何らかのデザインパターンを適用しているわけではありません**

```swift

 enum Keys: String {
     case name = "name"
     case nickname = "nickname"
 }
 
 final class ViewModel {
 
     private let userDefaults = UserDefaults.standard
 
     func getName() -> String {
         guard let name = userDefaults.string(forKey: Keys.name.rawValue) else { return "none" }
         return name
     }
 
     func setName(name: String) {
         userDefaults.set(name, forKey: Keys.name.rawValue)
     }
 }
```

**name** というkeyのString型の値を取得し書き込むclassです
これのテストclassは下記です

```swift
 final class ViewModelTests: XCTestCase {
 
     var dependency: Dependency!
 
     override func setUp() {
         super.setUp()
         dependency = Dependency()
     }
 
     func testGetnName() {
         let testTarget = dependency.testTarget
         XCTAssertEqual(testTarget.getName(), "none")
     }
 
 }
 
 extension ViewModelTests {
     struct Dependency {
         let testTarget: ViewModel
 
         init() {
             testTarget = .init()
         }
     }
 }
```

テスト関数である **testGetnName** は成功します
ではここで，ViewControllerを見てみましょう

```swift

 class ViewController: UIViewController {
 
     private let viewModel: ViewModel = .init()
 
     override func viewDidLoad() {
         super.viewDidLoad()
         viewModel.setName(name: "Ken")
     }
 
 }
```

アプリが起動した後に，**name** というkeyに対して**Ken**を書き込んでいます．
では，アプリを1度でも起動した後に，**testGetnName**を実行して見ましょう

失敗します
これは，既に**アプリ本体のUserDefaults**の**name**に**Ken**が書き込まれているためです
このことから，ViewModelのテストであるViewModelTestsが，アプリ本体のUserDefaultsに依存してることがわかります
これでは，テストコードとして十分に機能していません

# ViewModelを改善する
改善ポイントは，**init時にuserDefaultsに対してDI**をすることです
DIとはDependency Injectionの略称であり，日本語で依存性の注入といいます
詳しくは，
[猿でも分かる! Dependency Injection: 依存性の注入](https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f])
を御覧ください

今回はViewModelからアプリ本体のUserDefaultsへの依存を切り離します

まずViewModelを修正します

```swift

 final class ViewModel {
 
     private let userDefaults: UserDefaults // 修正箇所
 
     init(userDefaults: UserDefaults = UserDefaults.standard ) {
         self.userDefaults = userDefaults // これで準備OK
     }
 
     func getName() -> String {
         guard let name = userDefaults.string(forKey: Keys.name.rawValue) else { return "none" }
         return name
     }
 
     func setName(name: String) {
         userDefaults.set(name, forKey: Keys.name.rawValue)
     }
 }
```

init時に代入することにより，DIする準備が整いました
次に，テストコードを修正します

```swift
 final class ViewModelTests: XCTestCase {
 
     var dependency: Dependency!
 
     override func setUp() {
         super.setUp()
         dependency = Dependency()
     }
 
     func testGetnName() {
         let testTarget = dependency.testTarget
         XCTAssertEqual(testTarget.getName(), "none")
     }
 
 }
 
 extension ViewModelTests {
     struct Dependency {
         let testTarget: ViewModel
 
         init() {
             testTarget = .init(userDefaults: UserDefaults()) // 修正箇所
         }
     }
 }
```

これで，テストが成功する．．．！
と思いましたが，失敗します

原因は `testTarget = .init(userDefaults: UserDefaults())` です
`UserDefaults()`と`UserDefaults.standard`は同じものを指しています

```swift
 open class UserDefaults : NSObject {
 
     
     /**
      +standardUserDefaults returns a global instance of NSUserDefaults configured to search the current application's search list.
      */
     open class var standard: UserDefaults { get }
    ・
    ・
    ・
 }
```

Appleの公式ドキュメントより
https://developer.apple.com/documentation/foundation/userdefaults/1416603-standard
>class var standard: UserDefaults { get }
>The shared defaults object.

https://developer.apple.com/documentation/foundation/userdefaults/1414356-init
>Use this initializer only if you're not using the shared standard user defaults.

ここで登場するのが，`init(suiteName:)`です

# ViewModelTestsを改善する
`init(suiteName:)`は任意の名前でUserDefaultsを作ることができます
ただし，Apple公式ドキュメントより
https://developer.apple.com/documentation/foundation/userdefaults/1409957-init
> The argument and registration domains are shared between all instances of UserDefaults.
> 引数と登録ドメインは、UserDefaultsのすべてのインスタンス間で共有されます。

では何が嬉しいのか？
それは，

**任意のUserDefaultsからstandardの値を参照することはできますが，任意のUserDefaultsに値を書き込んでもstandardには影響がありません**

ということです
詳しくは
[UserDefaultsをテストする際にuserSuiteを利用する件](https://qiita.com/yimajo/items/05b86520ae633e73ff6c)
を御覧ください

検証してみましょう

```swift:ViewController.swift
 final class ViewController: UIViewController {
 
     private let viewModel: ViewModel = .init()
 
     override func viewDidLoad() {
         super.viewDidLoad()
         viewModel.setName(name: "Ken")
         
         // 検証用です 本来はいりません
         UserDefaults.standard.set("Kenty", forKey: Keys.nickname.rawValue)
         print(viewModel.getName())
     }
 
 }
```

実行結果

```
 Ken
```

結構無茶苦茶ですが，
**name**に**Ken**を，**nickname**に**Kenty**を書き込んでいます

```swift:ViewModelTests.swift
 final class ViewModelTests: XCTestCase {
 
     var dependency: Dependency!
 
     override func setUp() {
         super.setUp()
         dependency = Dependency()
     }
 
     override func tearDown() {
         super.tearDown()
         dependency.removeUserDefaults() // テストが終わったら，削除しましょう
     }
 
     func testGetnName() {
         let testTarget = dependency.testTarget
         XCTAssertEqual(testTarget.getName(), "none")
         
         // 検証用です 本来はいりません
         testTarget.setName(name: "Tom")
         print(UserDefaults.standard.string(forKey: Keys.nickname.rawValue))
     }
 
 }
 
 extension ViewModelTests {
     struct Dependency {
         let testTarget: ViewModel
         let userDefaults: UserDefaults
         static let suiteName: String = "Test"
 
         init() {
             userDefaults = UserDefaults(suiteName: ViewModelTests.Dependency.suiteName)!
             testTarget = .init(userDefaults: userDefaults)
         }
 
         func removeUserDefaults() {
             userDefaults.removePersistentDomain(forName: ViewModelTests.Dependency.suiteName)
         }
     }
 }
```

先程アプリを起動したので，standardのUserDefaultsには，"Ken"が書き込まれています
なので，
`XCTAssertEqual(testTarget.getName(), "none")`は失敗しそうですが，成功します
`userDefaults = UserDefaults(suiteName: ViewModelTests.Dependency.suiteName)!`のおかげです

必ずテストが終わったら，作成したUserDefaultsは削除しましょう
`dependency.removeUserDefaults() // テストが終わったら，削除しましょう`
削除しないと残ってしまいます

最後に，検証結果を見てみましょう

```swift
 // 検証用です 本来はいりません
          testTarget.setName(name: "Tom")
          print(UserDefaults.standard.string(forKey: Keys.nickname.rawValue))
```

によって，**name**を**Tom**に書き換えています．また，**nickname**を取得しています．
この時の実行結果は，
実行結果

```
 Kenty
```

です．
テストを実行した後に，再度アプリを起動します．
その実行結果は，

実行結果

```
 Ken
```

上記のことから，
> 任意のUserDefaultsからstandardの値を参照することはできますが，任意のUserDefaultsに値を書き込んでもstandardには影響がありません 

が証明されました！

# 最後に
UserDefaultsを含むclassをテストする時は，`init(suiteName:)`を使用しましょう！

コードも一覧として載せておきます

```swift:ViewController.swift
 final class ViewController: UIViewController {
 
     private let viewModel: ViewModel = .init()
 
     override func viewDidLoad() {
         super.viewDidLoad()
         viewModel.setName(name: "Ken")
     }
 
 }
```
```swift:ViewModel.swift
 enum Keys: String {
      case name = "name"
      case nickname = "nickname"
  }
  
 final class ViewModel {
 
     private let userDefaults: UserDefaults
 
     init(userDefaults: UserDefaults = UserDefaults.standard ) {
         self.userDefaults = userDefaults
     }
 
     func getName() -> String {
         guard let name = userDefaults.string(forKey: Keys.name.rawValue) else { return "none" }
         return name
     }
 
     func setName(name: String) {
         userDefaults.set(name, forKey: Keys.name.rawValue)
     }
 }
```
```swift:ViewModelTests.test
 final class ViewModelTests: XCTestCase {
 
     var dependency: Dependency!
 
     override func setUp() {
         super.setUp()
         dependency = Dependency()
     }
 
     override func tearDown() {
         super.tearDown()
         dependency.removeUserDefaults() // テストが終わったら，削除しましょう
     }
 
     func testGetnName() {
         let testTarget = dependency.testTarget
         XCTAssertEqual(testTarget.getName(), "none")
     }
 
 }
 
 extension ViewModelTests {
     struct Dependency {
         let testTarget: ViewModel
         let userDefaults: UserDefaults
         static let suiteName: String = "Test"
 
         init() {
             userDefaults = UserDefaults(suiteName: ViewModelTests.Dependency.suiteName)!
             testTarget = .init(userDefaults: userDefaults)
         }
 
         func removeUserDefaults() {
             userDefaults.removePersistentDomain(forName: ViewModelTests.Dependency.suiteName)
         }
     }
 }
```

# 参考文献
- [猿でも分かる! Dependency Injection: 依存性の注入](https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f])
- [UserDefaultsをテストする際にuserSuiteを利用する件](https://qiita.com/yimajo/items/05b86520ae633e73ff6c)
- [UserDefaultsの公式ドキュメント](https://developer.apple.com/documentation/foundation/userdefaults)