Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A bad, stateful, and impossible to unit test, 'onboarding manger'.
import Foundation
protocol OnboardingManagerListener: AnyObject {
func onboardingManagerReturned(profile: Profile)
func onboardingManagerFailed()
}
class BadOnboardingManager: UsernameCollectorListener, EmailCollectorListener, PhoneNumberCollectorListener, PasswordCollectorListener {
weak var listener: OnboardingManagerListener?
private let passwordCollectorLauncher: PasswordCollectorLauncher
init(passwordCollectorLauncher: PasswordCollectorLauncher) {
self.passwordCollectorLauncher = passwordCollectorLauncher
// TODO: inject username, email, and phone number collector launchers.
}
// Stored Data
private var username: String?
private var email: String?
private var phoneNumber: String?
private var password: String?
private var passwordIsValid: Bool = false
private var passwordIssue: PasswordIssue?
func start() {
// 0: collect username using `launchUsernameCollector`.
// once `usernameCollectorReturned` calls, continue.
// 1: collect using with `launchEmailCollector`.
// once `emailCollectorReturned` calls, continue.
// 2: collect phone number using `launchPhoneNumberCollector`.
// once `phoneNumberCollectorReturned` calls, continue.
// 3: collect potential password using `launchPasswordCollector`.
// once `passwordCollectorReturned` calls, continue.
// NOTE: 4-6 are implemented as `attemptFinish`.
// 4: if any collectors failed, message failure to our listener
// 5: if potential password is invalid go to (3).
// 6: construct Profile object and send it to our listener.
}
private func attemptFinish() {
guard let username = self.username,
let email = self.email,
let phoneNumber = self.phoneNumber,
let password = self.password
else {
listener?.onboardingManagerFailed()
return
}
validatePassword()
if passwordIsValid {
listener?.onboardingManagerReturned(profile: Profile(username: username,
email: email,
phoneNumber: phoneNumber,
password: password))
} else {
// Launch the password collector again with a for a better password.
passwordCollectorLauncher.launch(existingPasswordIssue: passwordIssue)
}
}
private func validatePassword() {
passwordIsValid = false
passwordIssue = nil
guard let username = self.username,
let email = self.email,
let phoneNumber = self.phoneNumber,
let password = self.password
else {
return
}
if password.count < 10 {
passwordIssue = .tooShort
}
if password.contains(username) {
passwordIssue = .containsUsername
}
if password.contains(email) {
passwordIssue = .containsEmail
}
if password.contains(phoneNumber) {
passwordIssue = .containsPhoneNumber
}
if !passwordContainsAtLeastOneNumber() {
passwordIssue = .noNumbers
}
if !passwordContainsAtLeastOneSpecialCharacter() {
passwordIssue = .noSpecialCharacters
}
passwordIsValid = true
}
// Listener callbacks
func passwordCollectorReturned(password: String){
attemptFinish()
}
func passwordCollectorFailed(){
listener?.onboardingManagerFailed()
}
// Sub-actions which trigger user input and listener callbacks
private func launchPasswordCollector(){
passwordCollectorLauncher.launch(existingPasswordIssue: nil)
}
// TODO: Unimplemented Password validation helpers
private func passwordContainsAtLeastOneNumber() -> Bool { fatalError("unimplemented") }
private func passwordContainsAtLeastOneSpecialCharacter() -> Bool { fatalError("unimplemented") }
// TODO: Unimplemented Sub-actions which trigger user input and listener callbacks
private func launchUsernameCollector(){ fatalError("unimplemented") }
private func launchEmailCollector(){ fatalError("unimplemented") }
private func launchPhoneNumberCollector(){ fatalError("unimplemented") }
// TODO: Unimplemented Listener callbacks
func usernameCollectorReturned(username: String){ fatalError("unimplemented")}
func usernameCollectorFailed(){ fatalError("unimplemented") }
func emailCollectorReturned(email: String){ fatalError("unimplemented") }
func emailCollectorFailed(){ fatalError("unimplemented") }
func phoneNumberCollectorReturned(number: String){ fatalError("unimplemented") }
func phoneNumberCollectorFailed(){ fatalError("unimplemented") }
func onboardingManagerListenerReturned(profile: Profile){ fatalError("unimplemented") }
func onboardingManagerListenerFailed(){ fatalError("unimplemented") }
}
import XCTest
class BadOnboardingManagerTests: XCTestCase {
func test_OnboardingManager() {
let mockPasswordCollectorLauncher = MockPasswordCollectorLauncher()
let mockListener = MockOnboardingManagerListener()
let onboardingManager = BadOnboardingManager(passwordCollectorLauncher: mockPasswordCollectorLauncher)
onboardingManager.listener = mockListener
onboardingManager.start()
// TODO: assert the username collector launches now.
onboardingManager.usernameCollectorReturned(username: "ValidUsername")
// TODO: assert the email collector launches now.
onboardingManager.emailCollectorReturned(email: "valid@example.com")
// TODO: assert the phone number collector launches now.
XCTAssertEqual(mockPasswordCollectorLauncher.launchCallCount, 0)
onboardingManager.phoneNumberCollectorReturned(number: "+11235554321")
XCTAssertEqual(mockPasswordCollectorLauncher.launchCallCount, 1)
XCTAssertEqual(mockPasswordCollectorLauncher.existingPasswordIssueValues.last, nil)
onboardingManager.passwordCollectorReturned(password: "ValidUsername$123")
XCTAssertEqual(mockPasswordCollectorLauncher.launchCallCount, 2)
XCTAssertEqual(mockPasswordCollectorLauncher.existingPasswordIssueValues.last, PasswordIssue.containsUsername)
onboardingManager.passwordCollectorReturned(password: "Password123")
XCTAssertEqual(mockPasswordCollectorLauncher.launchCallCount, 3)
XCTAssertEqual(mockPasswordCollectorLauncher.existingPasswordIssueValues.last, PasswordIssue.noSpecialCharacters)
XCTAssertEqual(mockListener.onboardingManagerReturnedCallCount, 0)
onboardingManager.passwordCollectorReturned(password: "ValidPassword$123")
XCTAssertEqual(mockListener.onboardingManagerReturnedCallCount, 1)
}
}
import Foundation
protocol OnboardingManagerListener: AnyObject {
func onboardingManagerReturned(profile: Profile)
func onboardingManagerFailed()
}
class OnboardingManager: UsernameCollectorListener, EmailCollectorListener, PhoneNumberCollectorListener, PasswordCollectorListener {
weak var listener: OnboardingManagerListener?
private let passwordCollectorLauncher: PasswordCollectorLauncher
init(passwordCollectorLauncher: PasswordCollectorLauncher) {
self.passwordCollectorLauncher = passwordCollectorLauncher
// TODO: inject username, email, and phone number collector launchers.
}
// Stored Data
private var username: String?
private var email: String?
private var phoneNumber: String?
private var password: String?
func start() {
// 0: collect username using `launchUsernameCollector`.
// once `usernameCollectorReturned` calls, continue.
// 1: collect using with `launchEmailCollector`.
// once `emailCollectorReturned` calls, continue.
// 2: collect phone number using `launchPhoneNumberCollector`.
// once `phoneNumberCollectorReturned` calls, continue.
// 3: collect potential password using `launchPasswordCollector`.
// once `passwordCollectorReturned` calls, continue.
// NOTE: 4-6 are implemented as `attemptFinish`.
// 4: if any collectors failed, message failure to our listener
// 5: if potential password is invalid go to (3).
// 6: construct Profile object and send it to our listener.
}
private func attemptFinish() {
guard let username = self.username,
let email = self.email,
let phoneNumber = self.phoneNumber,
let password = self.password
else {
listener?.onboardingManagerFailed()
return
}
let (passwordIsValid, passwordIssue) = OnboardingManager.validatePassword(password: password, username: username, email: email, phoneNumber: phoneNumber)
if passwordIsValid {
listener?.onboardingManagerReturned(profile: Profile(username: username,
email: email,
phoneNumber: phoneNumber,
password: password))
} else {
// Launch the password collector again with a for a better password.
passwordCollectorLauncher.launch(existingPasswordIssue: passwordIssue)
}
}
static func validatePassword(password: String, username: String, email: String, phoneNumber: String) -> (Bool, PasswordIssue?){
if password.count < 10 {
return (false, .tooShort)
}
if password.contains(username) {
return (false, .containsUsername)
}
if password.contains(email) {
return (false, .containsEmail)
}
if password.contains(phoneNumber) {
return (false, .containsPhoneNumber)
}
if !passwordContainsAtLeastOneNumber(password) {
return (false, .noNumbers)
}
if !passwordContainsAtLeastOneSpecialCharacter(password) {
return (false, .noSpecialCharacters)
}
return (true, nil)
}
// Listener callbacks
func passwordCollectorReturned(password: String){
attemptFinish()
}
func passwordCollectorFailed(){
listener?.onboardingManagerFailed()
}
// Sub-actions which trigger user input and listener callbacks
private func launchPasswordCollector(){
passwordCollectorLauncher.launch(existingPasswordIssue: nil)
}
// TODO: Unimplemented Password validation helpers
static func passwordContainsAtLeastOneNumber(_ password: String) -> Bool { fatalError("unimplemented") }
static func passwordContainsAtLeastOneSpecialCharacter(_ password: String) -> Bool { fatalError("unimplemented") }
// TODO: Unimplemented Sub-actions which trigger user input and listener callbacks
private func launchUsernameCollector(){ fatalError("unimplemented") }
private func launchEmailCollector(){ fatalError("unimplemented") }
private func launchPhoneNumberCollector(){ fatalError("unimplemented") }
// TODO: Unimplemented Listener callbacks
func usernameCollectorReturned(username: String){ fatalError("unimplemented")}
func usernameCollectorFailed(){ fatalError("unimplemented") }
func emailCollectorReturned(email: String){ fatalError("unimplemented") }
func emailCollectorFailed(){ fatalError("unimplemented") }
func phoneNumberCollectorReturned(number: String){ fatalError("unimplemented") }
func phoneNumberCollectorFailed(){ fatalError("unimplemented") }
func onboardingManagerListenerReturned(profile: Profile){ fatalError("unimplemented") }
func onboardingManagerListenerFailed(){ fatalError("unimplemented") }
}
import XCTest
class BetterOnboardingManagerTests: XCTestCase {
func testPasswordCanNotContainUsername() {
let (didPass, issue) = OnboardingManager.validatePassword(password: "ValidUsername$123",
username: "ValidUsername",
email: "valid@example.com",
phoneNumber: "+11235554321")
XCTAssertFalse(didPass)
XCTAssertEqual(issue, PasswordIssue.containsUsername)
}
func testPasswordMustHaveSpecialChar() {
let (didPass, issue) = OnboardingManager.validatePassword(password: "Password123",
username: "ValidUsername",
email: "valid@example.com",
phoneNumber: "+11235554321")
XCTAssertFalse(didPass)
XCTAssertEqual(issue, PasswordIssue.noSpecialCharacters)
}
func testValidPassword() {
let (didPass, issue) = OnboardingManager.validatePassword(password: "ValidUsername",
username: "Password$123",
email: "valid@example.com",
phoneNumber: "+11235554321")
XCTAssertFalse(didPass)
XCTAssertEqual(issue, PasswordIssue.noSpecialCharacters)
}
}
protocol UsernameCollectorListener {
func usernameCollectorReturned(username: String)
func usernameCollectorFailed()
}
protocol EmailCollectorListener {
func emailCollectorReturned(email: String)
func emailCollectorFailed()
}
protocol PhoneNumberCollectorListener {
func phoneNumberCollectorReturned(number: String)
func phoneNumberCollectorFailed()
}
protocol PasswordCollectorListener {
func passwordCollectorReturned(password: String)
func passwordCollectorFailed()
}
struct Profile {
let username: String
let email: String
let phoneNumber: String
let password: String
}
protocol PasswordCollectorLauncher {
func launch(prompt: String?)
}
class MockOnboardingManagerListener: OnboardingManagerListener {
var onboardingManagerReturnedCallCount = 0
var onboardingManagerReturnedProfileValues = [Profile]()
func onboardingManagerReturned(profile: Profile) {
onboardingManagerReturnedCallCount += 1
onboardingManagerReturnedProfileValues.append(profile)
}
var onboardingManagerFailedCallCount = 0
func onboardingManagerFailed() {
onboardingManagerFailedCallCount += 1
}
}
class MockPasswordCollectorLauncher: PasswordCollectorLauncher {
var launchCallCount = 0
var existingPasswordIssueValues = [PasswordIssue?]()
func launch(existingPasswordIssue: PasswordIssue?) {
launchCallCount += 1
existingPasswordIssueValues.append(existingPasswordIssue)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.