{"id":24497041,"url":"https://github.com/yannxou/publisherexpectations","last_synced_at":"2025-04-14T04:14:52.063Z","repository":{"id":99508092,"uuid":"605679360","full_name":"yannxou/PublisherExpectations","owner":"yannxou","description":"XCTestExpectation subclasses to simplify Publisher testing and improve test readability","archived":false,"fork":false,"pushed_at":"2024-02-05T15:17:35.000Z","size":37,"stargazers_count":4,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-14T04:14:30.237Z","etag":null,"topics":["combine","publisher","swift","unit-test","unit-testing","xcode","xctestexpectation"],"latest_commit_sha":null,"homepage":"https://tech.new-work.se/unit-testing-combine-publishers-6f581e30c370","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yannxou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-02-23T17:08:02.000Z","updated_at":"2024-01-26T10:21:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"f051b82d-3bd3-4180-930c-8d52b87d3fa5","html_url":"https://github.com/yannxou/PublisherExpectations","commit_stats":{"total_commits":18,"total_committers":2,"mean_commits":9.0,"dds":0.4444444444444444,"last_synced_commit":"224378e6263f06d050ead348e995a6e89f0d88eb"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yannxou%2FPublisherExpectations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yannxou%2FPublisherExpectations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yannxou%2FPublisherExpectations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yannxou%2FPublisherExpectations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yannxou","download_url":"https://codeload.github.com/yannxou/PublisherExpectations/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248819408,"owners_count":21166477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["combine","publisher","swift","unit-test","unit-testing","xcode","xctestexpectation"],"created_at":"2025-01-21T21:19:59.654Z","updated_at":"2025-04-14T04:14:52.045Z","avatar_url":"https://github.com/yannxou.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PublisherExpectations\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fyannxou%2FPublisherExpectations%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/yannxou/PublisherExpectations)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fyannxou%2FPublisherExpectations%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/yannxou/PublisherExpectations)\n\nXCTestExpectation subclasses that simplify testing Combine Publishers and help to improve the readability of unit tests.\n\n## Motivation\n\nWriting tests for Combine Publishers (or @Published properties) using `XCTestExpectation` usually involves some boilerplate code such as:\n\n```swift\nlet expectation = XCTestExpectation(description: \"Wait for the publisher to emit the expected value\")\nviewModel.$output.sink { _ in\n} receiveValue: { value in\n    if value == expectedValue {\n        expectation.fulfill()\n    }\n}\n.store(in: \u0026cancellables)\n\nwait(for: [expectation], timeout: 1)\n```\n\nWe can try using a `XCTKeyPathExpectation` but it requires that the observed object inherits from `NSObject` and also marking the properties we want to observe with both the `@objc` attribute and the `dynamic` modifier to make them KVO-compliant:\n\n```swift\nclass ViewModel: NSObject {\n    @objc dynamic var isLoading = false\n}\n\nlet expectation = XCTKeyPathExpectation(keyPath: \\ViewModel.isLoading, observedObject: viewModel, expectedValue: true)\n```\n\nAnother tempting approach would be using `XCTNSPredicateExpectation` like:\n\n```swift\nlet expectation = XCTNSPredicateExpectation(predicate: NSPredicate { _,_ in\n    viewModel.output == expectedValue\n}, object: viewModel)\n```\n\nWhile this looks nice and compact, the problem with `XCTNSPredicateExpectation` is that is quite slow and best suited for UI tests. This is because it uses some kind of polling mechanism that adds a significant delay of 1 second minimum before the expectation is fulfilled. So it's better not to follow this path in unit tests.\n\n## Description\n\nThe PublisherExpectations is a set of 3 XCTestExpectation that allows declaring expectations for publisher events in a clear and concise manner. They inherit from XCTestExpectation so they can be used in the `wait(for: [expectations])` call as with any other expectation. \n\n* `PublisherValueExpectation`: An expectation that is fulfilled when a publisher emits a value that matches a certain condition.\n* `PublisherFinishedExpectation`: An expectation that is fulfilled when a publisher completes successfully.\n* `PublisherFailureExpectation`: An expectation that is fulfilled when a publisher completes with a failure.\n\n## Usage\n\n### PublisherValueExpectation\n\n* Wait for an expected value:\n```swift\nlet publisherExpectation = PublisherValueExpectation(stringPublisher, expectedValue: \"Got it\")\n```\n\n* Wait for a value that matches a condition:\n```swift\nlet publisherExpectation = PublisherValueExpectation(arrayPublisher) { $0.contains(value) }\n```\n\n* Works with `@Published` property wrappers as well:\n```swift\nlet publisherExpectation = PublisherValueExpectation(viewModel.$isLoaded, expectedValue: true)\n```\n```swift\nlet publisherExpectation = PublisherValueExpectation(viewModel.$keywords) { $0.contains(\"Cool\") }\n```\n\n### PublisherFinishedExpectation\n\n* Waiting for the publisher to finish:\n```swift\nlet publisherExpectation = PublisherFinishedExpectation(publisher)\n```\n\n* Waiting for the publisher to finish after emitting an expected value:\n```swift\nlet publisherExpectation = PublisherFinishedExpectation(publisher, expectedValue: 2)\n```\n\n* Waiting to finish after emitting a value that matches a certain condition:\n```swift\nlet publisherExpectation = PublisherFinishedExpectation(arrayPublisher) { array in\n    array.allSatisfy { $0 \u003e 5 }\n}\n```\n\n### PublisherFailureExpectation\n\n* Expecting a failure:\n```swift\nlet publisherExpectation = PublisherFailureExpectation(publisher)\n```\n\n* Expecting a failure with an error that matches a condition:\n```swift\nlet publisherExpectation = PublisherFailureExpectation(publisher) { error in\n    guard case .apiError(let code) = error, code = 500 else { return false }\n    return true\n}\n```\n\n* If the expected error conforms to Equatable:\n```swift\nlet publisherExpectation = PublisherFailureExpectation(publisher, expectedError: ApiError(code: 100))\n```\n\n## Installation\n\n1. From the File menu, select Add Packages...\n2. Enter package repository URL: https://github.com/yannxou/PublisherExpectations\n3. Confirm the version and let Xcode resolve the package\n\n## Tips\n\nThanks to Combine we can adapt the publisher to check many things while keeping the test readability:\n\n* Expect many values to be emitted:\n```swift\nlet publisherExpectation = PublisherValueExpectation(publisher.collect(3), expectedValue: [1,2,3])\n```\n\n* Expect the first/last emitted value:\n```swift\nlet publisherExpectation = PublisherValueExpectation(publisher.first(), expectedValue: 1)\nlet publisherExpectation = PublisherValueExpectation(publisher.last(), expectedValue: 5)\n```\n\n* Expect the second emitted value:\n```swift\nlet publisherExpectation = PublisherValueExpectation(publisher.dropFirst().first(), expectedValue: 2)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyannxou%2Fpublisherexpectations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyannxou%2Fpublisherexpectations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyannxou%2Fpublisherexpectations/lists"}