Last active
June 6, 2024 17:53
-
-
Save damodarnamala/02f9ce88a31e72902bc5f29fbdfdef33 to your computer and use it in GitHub Desktop.
swift BDD examople
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ViewController.swift | |
// CleanCodeApp | |
// | |
// Created by Damodar Namala on 06/06/24. | |
// | |
import UIKit | |
class ViewController: UIViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// Do any additional setup after loading the view. | |
} | |
@IBAction func sendPost() { | |
//ussage | |
let viewModel = ViewModel(usecase: PostsUseCaseImpl()) | |
viewModel.send(input: .sendPost("Hi")) | |
} | |
} | |
class AnyUseCase<Input, Output>: UseCase { | |
private let _request: (Input, @escaping (Output) -> Void) -> Void | |
init<U: UseCase>(_ usecase: U) where U.InputType == Input, U.OutputType == Output { | |
_request = usecase.request | |
} | |
func request(input: Input, output: @escaping (Output) -> Void) { | |
_request(input, output) | |
} | |
} | |
protocol UseCase { | |
associatedtype InputType | |
associatedtype OutputType | |
func request(input: InputType, output: @escaping ((OutputType) -> Void)) | |
} | |
protocol PostsUseCaseOutput { | |
var description: String { get } | |
} | |
struct PostsUseCaseImpl: UseCase { | |
enum Input { | |
case getPost | |
case sendPost(_ name: String) | |
} | |
enum Output: PostsUseCaseOutput { | |
case receivedPosts([String]) | |
case sentPost(String) | |
var description: String { | |
switch self { | |
case .receivedPosts(let posts): | |
return "Received posts: \(posts)" | |
case .sentPost(let text): | |
return "Sent post: \(text)" | |
} | |
} | |
} | |
typealias InputType = Input | |
typealias OutputType = Output | |
func request(input: Input, output: @escaping ((Output) -> Void)) { | |
switch input { | |
case .getPost: | |
output(.receivedPosts(["123"])) | |
case .sendPost(let name): | |
output(.sentPost(name)) | |
} | |
} | |
} | |
class ViewModel { | |
var posts: [String] = [] | |
var usecase: AnyUseCase<PostsUseCaseImpl.Input, PostsUseCaseImpl.Output> | |
init<U: UseCase>(usecase: U) | |
where U.InputType == PostsUseCaseImpl.Input, U.OutputType == PostsUseCaseImpl.Output { | |
self.usecase = AnyUseCase(usecase) | |
} | |
func send(input: PostsUseCaseImpl.Input) { | |
self.usecase.request(input: input) { response in | |
switch response { | |
case .receivedPosts(let posts): | |
self.posts = posts | |
case .sentPost(let text): | |
print(text) | |
} | |
} | |
} | |
} | |
class MockPostsUseCase: UseCase { | |
typealias InputType = PostsUseCaseImpl.Input | |
typealias OutputType = PostsUseCaseImpl.Output | |
var output: ((OutputType) -> Void)? | |
func request(input: InputType, output: @escaping ((OutputType) -> Void)) { | |
self.output = output | |
switch input { | |
case .getPost: | |
output(.receivedPosts(["MockPost1", "MockPost2"])) | |
case .sendPost(let name): | |
output(.sentPost(name)) | |
} | |
} | |
} | |
// Testing | |
import Quick | |
import Nimble | |
@testable import CleanCodeApp | |
class ViewModelSpec: QuickSpec { | |
override class func setUp() { | |
super.setUp() | |
} | |
override class func spec() { | |
describe("When ViewMode Usecase Request Sent") { | |
var viewModel: ViewModel! | |
var mockUseCase: MockPostsUseCase! | |
beforeEach { | |
mockUseCase = MockPostsUseCase() | |
viewModel = ViewModel(usecase: mockUseCase) | |
} | |
it("should print received posts when getPost is called") { | |
viewModel.send(input: .getPost) | |
mockUseCase.output?(.receivedPosts(["TestPost1", "TestPost2"])) | |
expect(viewModel.posts.first).notTo(beNil()) | |
expect(viewModel.posts.first).to(equal("TestPost1")) | |
// expect(viewModel.posts.first).to(beEmpty()) | |
} | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol UseCase { | |
associatedtype InputType | |
associatedtype OutputType | |
func request(input: InputType, output: @escaping ((Result<OutputType, Error>) -> Void)) | |
} | |
struct PostsUseCaseImpl: UseCase { | |
enum Input { | |
case getPost | |
case sendPost(_ name: String) | |
} | |
enum Output { | |
case receivedPosts([String]) | |
case sentPost(String) | |
} | |
typealias InputType = Input | |
typealias OutputType = Output | |
func request(input: Input, output: @escaping ((Result<Output, Error>) -> Void)) { | |
switch input { | |
case .getPost: | |
output(.success(.receivedPosts(["123"]))) | |
case .sendPost(let name): | |
output(.success(.sentPost(name))) | |
} | |
} | |
} | |
class AnyUseCase<Input, Output>: UseCase { | |
private let _request: (Input, @escaping (Result<Output, Error>) -> Void) -> Void | |
init<U: UseCase>(_ usecase: U) where U.InputType == Input, U.OutputType == Output { | |
_request = usecase.request | |
} | |
func request(input: Input, output: @escaping (Result<Output, Error>) -> Void) { | |
_request(input, output) | |
} | |
} | |
import Combine | |
class ViewModel: ObservableObject { | |
var usecase: AnyUseCase<PostsUseCaseImpl.Input, PostsUseCaseImpl.Output> | |
@Published var posts: [String] = [] | |
@Published var sentPostText: String = "" | |
@Published var errorMessage: String? | |
init<U: UseCase>(usecase: U) where U.InputType == PostsUseCaseImpl.Input, U.OutputType == PostsUseCaseImpl.Output { | |
self.usecase = AnyUseCase(usecase) | |
} | |
func send(input: PostsUseCaseImpl.Input) { | |
self.usecase.request(input: input) { [weak self] result in | |
switch result { | |
case .success(let response): | |
switch response { | |
case .receivedPosts(let posts): | |
self?.posts = posts | |
case .sentPost(let text): | |
self?.sentPostText = text | |
} | |
case .failure(let error): | |
self?.errorMessage = error.localizedDescription | |
} | |
} | |
} | |
} | |
class MockPostsUseCase: UseCase { | |
typealias InputType = PostsUseCaseImpl.Input | |
typealias OutputType = PostsUseCaseImpl.Output | |
var output: ((Result<OutputType, Error>) -> Void)? | |
func request(input: InputType, output: @escaping ((Result<OutputType, Error>) -> Void)) { | |
self.output = output | |
switch input { | |
case .getPost: | |
output(.success(.receivedPosts(["MockPost1", "MockPost2"]))) | |
case .sendPost(let name): | |
output(.success(.sentPost(name))) | |
} | |
} | |
} | |
import Quick | |
import Nimble | |
import Combine | |
@testable import YourAppName | |
class ViewModelSpec: QuickSpec { | |
override func spec() { | |
describe("ViewModel") { | |
var viewModel: ViewModel! | |
var mockUseCase: MockPostsUseCase! | |
var cancellables: Set<AnyCancellable>! | |
beforeEach { | |
mockUseCase = MockPostsUseCase() | |
viewModel = ViewModel(usecase: mockUseCase) | |
cancellables = [] | |
} | |
it("should update posts when getPost is called") { | |
var posts: [String] = [] | |
viewModel.$posts | |
.sink { posts = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .getPost) | |
mockUseCase.output?(.success(.receivedPosts(["TestPost1", "TestPost2"]))) | |
expect(posts).toEventually(equal(["TestPost1", "TestPost2"])) | |
} | |
it("should update sentPostText when sendPost is called") { | |
var sentPostText: String = "" | |
viewModel.$sentPostText | |
.sink { sentPostText = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .sendPost("Hello")) | |
mockUseCase.output?(.success(.sentPost("Hello"))) | |
expect(sentPostText).toEventually(equal("Hello")) | |
} | |
it("should update errorMessage on error") { | |
var errorMessage: String? | |
viewModel.$errorMessage | |
.sink { errorMessage = $0 } | |
.store(in: &cancellables) | |
let mockError = NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Mock Error"]) | |
viewModel.send(input: .getPost) | |
mockUseCase.output?(.failure(mockError)) | |
expect(errorMessage).toEventually(equal("Mock Error")) | |
} | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum UseCaseError: Error { | |
case networkError | |
case unknownError | |
} | |
protocol UseCase { | |
associatedtype InputType | |
associatedtype OutputType | |
func request(input: InputType, output: @escaping ((Result<OutputType, UseCaseError>) -> Void)) | |
} | |
struct PostsUseCaseImpl: UseCase { | |
enum Input { | |
case getPost | |
case sendPost(_ name: String) | |
} | |
enum Output { | |
case receivedPosts([String]) | |
case sentPost(String) | |
} | |
typealias InputType = Input | |
typealias OutputType = Output | |
func request(input: Input, output: @escaping ((Result<Output, UseCaseError>) -> Void)) { | |
switch input { | |
case .getPost: | |
output(.success(.receivedPosts(["123"]))) | |
case .sendPost(let name): | |
output(.success(.sentPost(name))) | |
} | |
} | |
} | |
import Combine | |
import os.log | |
class ViewModel: ObservableObject { | |
var usecase: AnyUseCase<PostsUseCaseImpl.Input, PostsUseCaseImpl.Output> | |
@Published var posts: [String] = [] | |
@Published var sentPostText: String = "" | |
@Published var errorMessage: String? | |
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewModel") | |
init<U: UseCase>(usecase: U) where U.InputType == PostsUseCaseImpl.Input, U.OutputType == PostsUseCaseImpl.Output { | |
self.usecase = AnyUseCase(usecase) | |
} | |
func send(input: PostsUseCaseImpl.Input) { | |
self.usecase.request(input: input) { [weak self] result in | |
guard let self = self else { return } | |
switch result { | |
case .success(let response): | |
switch response { | |
case .receivedPosts(let posts): | |
self.posts = posts | |
case .sentPost(let text): | |
self.sentPostText = text | |
} | |
case .failure(let error): | |
self.handleError(error) | |
} | |
} | |
} | |
private func handleError(_ error: UseCaseError) { | |
switch error { | |
case .networkError: | |
self.errorMessage = "Network error occurred. Please try again later." | |
case .unknownError: | |
self.errorMessage = "An unknown error occurred." | |
} | |
logger.error("Use case error: \(error.localizedDescription)") | |
} | |
} | |
class MockPostsUseCase: UseCase { | |
typealias InputType = PostsUseCaseImpl.Input | |
typealias OutputType = PostsUseCaseImpl.Output | |
var output: ((Result<OutputType, UseCaseError>) -> Void)? | |
func request(input: InputType, output: @escaping ((Result<OutputType, UseCaseError>) -> Void)) { | |
self.output = output | |
switch input { | |
case .getPost: | |
output(.success(.receivedPosts(["MockPost1", "MockPost2"]))) | |
case .sendPost(let name): | |
output(.success(.sentPost(name))) | |
} | |
} | |
} | |
import Quick | |
import Nimble | |
import Combine | |
@testable import YourAppName | |
class ViewModelSpec: QuickSpec { | |
override func spec() { | |
describe("ViewModel") { | |
var viewModel: ViewModel! | |
var mockUseCase: MockPostsUseCase! | |
var cancellables: Set<AnyCancellable>! | |
beforeEach { | |
mockUseCase = MockPostsUseCase() | |
viewModel = ViewModel(usecase: mockUseCase) | |
cancellables = [] | |
} | |
it("should update posts when getPost is called") { | |
var posts: [String] = [] | |
viewModel.$posts | |
.sink { posts = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .getPost) | |
mockUseCase.output?(.success(.receivedPosts(["TestPost1", "TestPost2"]))) | |
expect(posts).toEventually(equal(["TestPost1", "TestPost2"])) | |
} | |
it("should update sentPostText when sendPost is called") { | |
var sentPostText: String = "" | |
viewModel.$sentPostText | |
.sink { sentPostText = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .sendPost("Hello")) | |
mockUseCase.output?(.success(.sentPost("Hello"))) | |
expect(sentPostText).toEventually(equal("Hello")) | |
} | |
it("should update errorMessage on error") { | |
var errorMessage: String? | |
viewModel.$errorMessage | |
.sink { errorMessage = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .getPost) | |
mockUseCase.output?(.failure(.networkError)) | |
expect(errorMessage).toEventually(equal("Network error occurred. Please try again later.")) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum ViewModelState { | |
case idle | |
case loading | |
case finished | |
case error(String) | |
} | |
import Combine | |
import os.log | |
class ViewModel: ObservableObject { | |
var usecase: AnyUseCase<PostsUseCaseImpl.Input, PostsUseCaseImpl.Output> | |
@Published var posts: [String] = [] | |
@Published var sentPostText: String = "" | |
@Published var state: ViewModelState = .idle | |
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewModel") | |
init<U: UseCase>(usecase: U) where U.InputType == PostsUseCaseImpl.Input, U.OutputType == PostsUseCaseImpl.Output { | |
self.usecase = AnyUseCase(usecase) | |
} | |
func send(input: PostsUseCaseImpl.Input) { | |
self.state = .loading | |
self.usecase.request(input: input) { [weak self] result in | |
guard let self = self else { return } | |
switch result { | |
case .success(let response): | |
switch response { | |
case .receivedPosts(let posts): | |
self.posts = posts | |
self.state = .finished | |
case .sentPost(let text): | |
self.sentPostText = text | |
self.state = .finished | |
} | |
case .failure(let error): | |
self.handleError(error) | |
} | |
} | |
} | |
private func handleError(_ error: UseCaseError) { | |
let errorMessage: String | |
switch error { | |
case .networkError: | |
errorMessage = "Network error occurred. Please try again later." | |
case .unknownError: | |
errorMessage = "An unknown error occurred." | |
} | |
self.state = .error(errorMessage) | |
logger.error("Use case error: \(errorMessage)") | |
} | |
} | |
class MockPostsUseCase: UseCase { | |
typealias InputType = PostsUseCaseImpl.Input | |
typealias OutputType = PostsUseCaseImpl.Output | |
var output: ((Result<OutputType, UseCaseError>) -> Void)? | |
func request(input: InputType, output: @escaping ((Result<OutputType, UseCaseError>) -> Void)) { | |
self.output = output | |
switch input { | |
case .getPost: | |
output(.success(.receivedPosts(["MockPost1", "MockPost2"]))) | |
case .sendPost(let name): | |
output(.success(.sentPost(name))) | |
} | |
} | |
} | |
import Quick | |
import Nimble | |
import Combine | |
@testable import YourAppName | |
class ViewModelSpec: QuickSpec { | |
override func spec() { | |
describe("ViewModel") { | |
var viewModel: ViewModel! | |
var mockUseCase: MockPostsUseCase! | |
var cancellables: Set<AnyCancellable>! | |
beforeEach { | |
mockUseCase = MockPostsUseCase() | |
viewModel = ViewModel(usecase: mockUseCase) | |
cancellables = [] | |
} | |
it("should update posts and state when getPost is called") { | |
var posts: [String] = [] | |
var state: ViewModelState = .idle | |
viewModel.$posts | |
.sink { posts = $0 } | |
.store(in: &cancellables) | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .getPost) | |
expect(state).to(equal(.loading)) | |
mockUseCase.output?(.success(.receivedPosts(["TestPost1", "TestPost2"]))) | |
expect(posts).toEventually(equal(["TestPost1", "TestPost2"])) | |
expect(state).toEventually(equal(.finished)) | |
} | |
it("should update sentPostText and state when sendPost is called") { | |
var sentPostText: String = "" | |
var state: ViewModelState = .idle | |
viewModel.$sentPostText | |
.sink { sentPostText = $0 } | |
.store(in: &cancellables) | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .sendPost("Hello")) | |
expect(state).to(equal(.loading)) | |
mockUseCase.output?(.success(.sentPost("Hello"))) | |
expect(sentPostText).toEventually(equal("Hello")) | |
expect(state).toEventually(equal(.finished)) | |
} | |
it("should update errorMessage and state on error") { | |
var errorMessage: String? | |
var state: ViewModelState = .idle | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.send(input: .getPost) | |
expect(state).to(equal(.loading)) | |
mockUseCase.output?(.failure(.networkError)) | |
if case let .error(message) = state { | |
errorMessage = message | |
} | |
expect(errorMessage).toEventually(equal("Network error occurred. Please try again later.")) | |
expect(state).toEventually(equal(.error("Network error occurred. Please try again later."))) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol NetworkService { | |
func fetchPosts(completion: @escaping (Result<[String], UseCaseError>) -> Void) | |
func sendPost(name: String, completion: @escaping (Result<String, UseCaseError>) -> Void) | |
} | |
class PostsNetworkService: NetworkService { | |
func fetchPosts(completion: @escaping (Result<[String], UseCaseError>) -> Void) { | |
// Simulate network request | |
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { | |
completion(.success(["Post1", "Post2", "Post3"])) | |
} | |
} | |
func sendPost(name: String, completion: @escaping (Result<String, UseCaseError>) -> Void) { | |
// Simulate network request | |
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { | |
completion(.success(name)) | |
} | |
} | |
} | |
protocol GetPostsUseCase { | |
func execute(completion: @escaping (Result<[String], UseCaseError>) -> Void) | |
} | |
protocol SendPostUseCase { | |
func execute(name: String, completion: @escaping (Result<String, UseCaseError>) -> Void) | |
} | |
class GetPostsUseCaseImpl: GetPostsUseCase { | |
private let networkService: NetworkService | |
init(networkService: NetworkService) { | |
self.networkService = networkService | |
} | |
func execute(completion: @escaping (Result<[String], UseCaseError>) -> Void) { | |
networkService.fetchPosts(completion: completion) | |
} | |
} | |
class SendPostUseCaseImpl: SendPostUseCase { | |
private let networkService: NetworkService | |
init(networkService: NetworkService) { | |
self.networkService = networkService | |
} | |
func execute(name: String, completion: @escaping (Result<String, UseCaseError>) -> Void) { | |
networkService.sendPost(name: name, completion: completion) | |
} | |
} | |
import Combine | |
import os.log | |
class ViewModel: ObservableObject { | |
@Published var posts: [String] = [] | |
@Published var sentPostText: String = "" | |
@Published var state: ViewModelState = .idle | |
private let getPostsUseCase: GetPostsUseCase | |
private let sendPostUseCase: SendPostUseCase | |
private var cancellables = Set<AnyCancellable>() | |
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewModel") | |
init(getPostsUseCase: GetPostsUseCase, sendPostUseCase: SendPostUseCase) { | |
self.getPostsUseCase = getPostsUseCase | |
self.sendPostUseCase = sendPostUseCase | |
} | |
func fetchPosts() { | |
self.state = .loading | |
getPostsUseCase.execute { [weak self] result in | |
guard let self = self else { return } | |
DispatchQueue.main.async { | |
switch result { | |
case .success(let posts): | |
self.posts = posts | |
self.state = .finished | |
case .failure(let error): | |
self.handleError(error) | |
} | |
} | |
} | |
} | |
func post(name: String) { | |
self.state = .loading | |
sendPostUseCase.execute(name: name) { [weak self] result in | |
guard let self = self else { return } | |
DispatchQueue.main.async { | |
switch result { | |
case .success(let text): | |
self.sentPostText = text | |
self.state = .finished | |
case .failure(let error): | |
self.handleError(error) | |
} | |
} | |
} | |
} | |
private func handleError(_ error: UseCaseError) { | |
let errorMessage: String | |
switch error { | |
case .networkError: | |
errorMessage = "Network error occurred. Please try again later." | |
case .unknownError: | |
errorMessage = "An unknown error occurred." | |
} | |
self.state = .error(errorMessage) | |
logger.error("Use case error: \(errorMessage)") | |
} | |
} | |
import Quick | |
import Nimble | |
import Combine | |
@testable import YourAppName | |
class ViewModelSpec: QuickSpec { | |
override func spec() { | |
describe("ViewModel") { | |
var viewModel: ViewModel! | |
var mockNetworkService: MockNetworkService! | |
var getPostsUseCase: GetPostsUseCaseImpl! | |
var sendPostUseCase: SendPostUseCaseImpl! | |
var cancellables: Set<AnyCancellable>! | |
beforeEach { | |
mockNetworkService = MockNetworkService() | |
getPostsUseCase = GetPostsUseCaseImpl(networkService: mockNetworkService) | |
sendPostUseCase = SendPostUseCaseImpl(networkService: mockNetworkService) | |
viewModel = ViewModel(getPostsUseCase: getPostsUseCase, sendPostUseCase: sendPostUseCase) | |
cancellables = [] | |
} | |
it("should update posts and state when fetchPosts is called") { | |
var posts: [String] = [] | |
var state: ViewModelState = .idle | |
viewModel.$posts | |
.sink { posts = $0 } | |
.store(in: &cancellables) | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.fetchPosts() | |
expect(state).to(equal(.loading)) | |
mockNetworkService.fetchPostsCompletion?(.success(["TestPost1", "TestPost2"])) | |
expect(posts).toEventually(equal(["TestPost1", "TestPost2"])) | |
expect(state).toEventually(equal(.finished)) | |
} | |
it("should update sentPostText and state when post is called") { | |
var sentPostText: String = "" | |
var state: ViewModelState = .idle | |
viewModel.$sentPostText | |
.sink { sentPostText = $0 } | |
.store(in: &cancellables) | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.post(name: "Hello") | |
expect(state).to(equal(.loading)) | |
mockNetworkService.sendPostCompletion?(.success("Hello")) | |
expect(sentPostText).toEventually(equal("Hello")) | |
expect(state).toEventually(equal(.finished)) | |
} | |
it("should update errorMessage and state on error") { | |
var errorMessage: String? | |
var state: ViewModelState = .idle | |
viewModel.$state | |
.sink { state = $0 } | |
.store(in: &cancellables) | |
viewModel.fetchPosts() | |
expect(state).to(equal(.loading)) | |
mockNetworkService.fetchPostsCompletion?(.failure(.networkError)) | |
if case let .error(message) = state { | |
errorMessage = message | |
} | |
expect(errorMessage).toEventually(equal("Network error occurred. Please try again later.")) | |
expect(state).toEventually(equal(.error("Network error occurred. Please try again later."))) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment