Skip to content

Instantly share code, notes, and snippets.

@damodarnamala
Last active June 6, 2024 17:53
Show Gist options
  • Save damodarnamala/02f9ce88a31e72902bc5f29fbdfdef33 to your computer and use it in GitHub Desktop.
Save damodarnamala/02f9ce88a31e72902bc5f29fbdfdef33 to your computer and use it in GitHub Desktop.
swift BDD examople
//
// 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())
}
}
}
}
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"))
}
}
}
}
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."))
}
}
}
}
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.")))
}
}
}
}
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