Saya mencoba menguji penerbit sederhana dalam kerangka Combine dan SwiftUI. Pengujian saya menguji bool yang diterbitkan bernama isValid dalam model tampilan saya. Model tampilan saya juga memiliki string nama pengguna yang diterbitkan, yang ketika berubah dan menjadi 3 karakter atau lebih isValid diberi nilai. Berikut model tampilannya. Saya yakin saya tidak mengerti bagaimana penerbit bekerja di lingkungan pengujian, waktu dll ... Terima kasih sebelumnya.

public class UserViewModel: ObservableObject {
  @Published var username = ""
  @Published var isValid = false
  private var disposables = Set<AnyCancellable>()

  init() {
    $username
      .receive(on: RunLoop.main)
      .removeDuplicates()
      .map { input in
        print("~~~> \(input.count >= 3)")
        return input.count >= 3
    }
    .assign(to: \.isValid, on: self)
    .store(in: &disposables)
  }
}

Ini pandangan saya, tidak terlalu penting di sini

struct ContentView: View {
  @ObservedObject private var userViewModel = UserViewModel()
  var body: some View {
    TextField("Username", text: $userViewModel.username)
  }
}

Ini file pengujian saya dan tes tunggal yang gagal

class StackoverFlowQuestionTests: XCTestCase {
  var model = UserViewModel()

    override func setUp() {
        model = UserViewModel()
    }

    override func tearDown() {
    }

    func testIsValid() {
      model.username = "1"
      XCTAssertFalse(model.isValid)
      model.username = "1234"
      XCTAssertTrue(model.isValid) //<----- THIS FAILS HERE
    }

}
9
user1302387 11 Januari 2020, 04:32

2 jawaban

Jawaban Terbaik

Alasannya adalah model tampilan tidak sinkron tetapi pengujian sinkron ...

$username
  .receive(on: RunLoop.main)

... operator .receive di sini membuat tugas akhir isValid pada siklus peristiwa berikutnya RunLoop.main

Tapi ujian

model.username = "1234"
XCTAssertTrue(model.isValid) //<----- THIS FAILS HERE

Mengharapkan isValid akan segera diubah.

Jadi ada kemungkinan solusi berikut:

  1. hapus operator .receive sama sekali (dalam hal ini lebih disukai, karena ini adalah alur kerja UI, yang selalu berada di runloop utama, jadi menggunakan penerimaan terjadwal adalah mubazir.

    $username
        .removeDuplicates()
        .map { input in
            print("~~~> \(input.count >= 3)")
            return input.count >= 3
        }
    .assign(to: \.isValid, on: self)
    .store(in: &disposables)
    

Hasil:

model.username = "1234"
XCTAssertTrue(model.isValid) // << PASSED
  1. buat UT menunggu satu peristiwa dan baru kemudian uji isValid (dalam hal ini harus didokumentasikan bahwa isValid memiliki sifat asinkron dengan niat)

    model.username = "1234"
    RunLoop.main.run(mode: .default, before: .distantPast) // << wait one event
    XCTAssertTrue(model.isValid) // << PASSED
    
4
Olcay Ertaş 24 Juli 2020, 12:38

Seperti yang dikatakan @Asperi: alasan kesalahan ini adalah Anda menerima nilai secara tidak sinkron. Saya menelusuri sedikit dan menemukan tutorial Apple tentang penggunaan XCTestExpectation. Jadi saya mencoba menggunakannya dengan kode Anda dan tes berhasil lulus. Cara lainnya adalah dengan menggunakan Gabungkan Harapan.

class StackoverFlowQuestionTests: XCTestCase {

    var model = UserViewModel()

    override func setUp() {
        model = UserViewModel()
    }

    func testIsValid() throws {

        let expectation = self.expectation(description: "waiting validation")

        let subscriber = model.$isValid.sink { _ in
            guard self.model.username != "" else { return }
            expectation.fulfill()
        }

        model.username = "1234"
        wait(for: [expectation], timeout: 1)
        XCTAssertTrue(model.isValid)

    }

    func testIsNotValid() {

        let expectation = self.expectation(description: "waiting validation")

        let subscriber = model.$isValid.sink { _ in
            guard self.model.username != "" else { return }
            expectation.fulfill()
        }

        model.username = "1"
        wait(for: [expectation], timeout: 1)
        XCTAssertFalse(model.isValid)

    }
}

PERBARUI Saya menambahkan semua kode dan output untuk kejelasan. Saya mengubah validasi pengujian seperti pada contoh Anda (di mana Anda menguji opsi "1" dan "1234"). Dan Anda akan melihat, bahwa saya hanya menyalin-tempel model Anda (kecuali nama dan public untuk variabel dan init()). Tapi tetap saja, saya tidak kesalahan ini:

Gagal menunggu asinkron: Melebihi batas waktu 1 detik, dengan harapan yang tidak terpenuhi: "menunggu validasi".

// MARK: TestableCombineModel.swift file
import Foundation
import Combine

public class TestableModel: ObservableObject {

    @Published public var username = ""
    @Published public var isValid = false
    private var disposables = Set<AnyCancellable>()

    public init() {
        $username
            .receive(on: RunLoop.main) // as you see, I didn't delete it
            .removeDuplicates()
            .map { input in
                print("~~~> \(input.count >= 3)")
                return input.count >= 3
        }
        .assign(to: \.isValid, on: self)
        .store(in: &disposables)
    }

}

// MARK: stackoverflowanswerTests.swift file:
import XCTest
import stackoverflowanswer
import Combine

class stackoverflowanswerTests: XCTestCase {

    var model: TestableModel!

    override func setUp() {
        model = TestableModel()
    }

    func testValidation() throws {

        let expectationSuccessfulValidation = self.expectation(description: "waiting successful validation")
        let expectationFailedValidation = self.expectation(description: "waiting failed validation")

        let subscriber = model.$isValid.sink { _ in
            // look at the output. at the first time there will be "nothing"
            print(self.model.username == "" ? "nothing" : self.model.username)
            if self.model.username == "1234" {
                expectationSuccessfulValidation.fulfill()
            } else if self.model.username == "1" {
                expectationFailedValidation.fulfill()
            }

        }

        model.username = "1234"
        wait(for: [expectationSuccessfulValidation], timeout: 1)
        XCTAssertTrue(model.isValid)

        model.username = "1"
        wait(for: [expectationFailedValidation], timeout: 1)
        XCTAssertFalse(model.isValid)

    }

}

Dan inilah keluarannya

2020-01-14 09:16:41.207649+0600 stackoverflowanswer[1266:46298] Launching with XCTest injected. Preparing to run tests.
2020-01-14 09:16:41.389610+0600 stackoverflowanswer[1266:46298] Waiting to run tests until the app finishes launching.
Test Suite 'All tests' started at 2020-01-14 09:16:41.711
Test Suite 'stackoverflowanswerTests.xctest' started at 2020-01-14 09:16:41.712
Test Suite 'stackoverflowanswerTests' started at 2020-01-14 09:16:41.712
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' started.
nothing
~~~> true
1234
~~~> false
1
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' passed (0.004 seconds).
Test Suite 'stackoverflowanswerTests' passed at 2020-01-14 09:16:41.717.
     Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'stackoverflowanswerTests.xctest' passed at 2020-01-14 09:16:41.717.
     Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'All tests' passed at 2020-01-14 09:16:41.718.
     Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.006) seconds

PERBARUI 2 Sebenarnya saya menangkap kesalahan "Penantian asinkron gagal: ..." jika saya mengubah baris kode ini:

let subscriber = model.$isValid.sink { _ in

Untuk ini, seperti yang diusulkan Xcode:

model.$isValid.sink { _ in // remove "let subscriber ="
1
Александр Грабовский 14 Januari 2020, 03:37