Test-Driven Development: #5 Refactoring Validation

Test-Driven Development: #5 Refactoring Validation

Now that it's all green and checkmark-y, let's start improving our structure a bit!

ยท

5 min read

So far it's been nothing but red to green (and fixing some compile errors), but less refactoring, I think with the current concepts discussed in the previous posts, we can safely start doing some refactoring and see how can we improve upon our current structure!

Introduction

Howdy, everyone! ๐Ÿ™Œ Friends: Joey saying how you doing

Been a journey thru the realms of TDD that was full of green and checkmarks!, actually coming to revise it, we now have 4 tests, each teaching us something, but today, instead of learning more knowledge, let's take this post today into reinforcing what we've learned, hence, let's better our structure, and with tests in place, we know that if something break, they would tell us!

Alright, load up your toolkit cuz we got some structure to improve!

Arnold Schwarzenegger Gearing Up

First things first, what do I don't like about the current structure?

Gotta be a reason for refactoring things, right?

  1. Our validations, they are not OCP friendly
  2. Our error handling needs to be more streamlined and more organized
  3. Test Coverage, this can be improved, and we will see how

So let's go from easy to difficult

Improving Validation Structure

Problem

Currently, we are hardcoding behavior with what we're trying to validate, like here

public enum ValidationError: Error {
  case emailIsEmpty
}

While this is ok... but in Validations, in its nature, there is some sort of abstraction among requirements

You will see that, something can't be empty, something else can't be invalid, something totally different must have this... etc

What if we dealt with email as something, and suddenly, we are testing passwords, phone numbers, emails, and any future field? wouldn't that be nicer?

so let's take this one step at a time

Improving OCP

Let's say we wanted to centralize our Business Configurations, y'know, those stuff like email can't be empty, a password can't be less than 8 characters, and so on...

Checking our requirements

Let's first get what the business needs in the validation requirements and then we can go on

Login Local Validation Requirements
1. Email
- Mandatory
- Must be Valid
- Can be limited to 999 characters in the field

2. Password
- Mandatory
- Must have at least 8 characters
- Must contain at least 1 special character
- Must have upper and lowercase character

Coming from these requirements, we can actually translate them into a centralized enum and start giving it some specs, starting off with something like this

/// Represents Business Logic Configuration for easier changes whether global or specific changes
public enum BusinessConfigurations {
    public enum Validation {}
}
public extension BusinessConfigurations.Validation {
  enum Field {
    case email
    case password

    var minMax: (min: Int, max: Int) {
      switch self {
      case .email:
        return (0, 999)
      case .password:
        return (8, 32)
      }
    }

    var title: String {
      switch self {
      case .email:
        return "Email"
      case .password:
        return "Password"
      }
    }
  }
}

Now, the next step is to edit our ValidationError enum to accept Field rather than hardcoding what is the error

public enum ValidationError: Error {
  case empty(BusinessConfigurations.Validation.Field) <- Changed
}

extension ValidationError: LocalizedError {

  public var errorDescription: String? {
    switch self {
    case let .empty(field): <- Changed
      return "\(field.title) can't be empty" <- Changed
    }
  }
}

extension ValidationError: Equatable { }

Now, instead of saying, emptyPassword, emptyEmail, I can just throw an error like...

guard email.isEmpty == false else { throw ValidationError.empty(.email) }
// More validations...

Now, let's try to fix the compile errors, let's visit our LoginViewModel again

func login(email: String, password: String) {
    if email.isEmpty {
//      validationErrors.append(.emailIsEmpty) <- Removed
      validationErrors.append(.empty(.email)) <- Added
    }
// More code ahead...

Well, an improvement, but there is still a lot, first let's adapt our testing code, and get back here for more refactoring

  func test_GivenEmptyEmail_WhenLogin_ShowError() {
    // Given
    let email = ""
    let password = ""

    // When
    sut.login(email: email, password: password)

    // Then
    XCTAssertTrue(sut.validationErrors.contains(.empty(.email)))
    // โ˜๏ธ Changed
  }

  func test_GivenAnError_WhenLogin_OnErrorIsCalled() {
    // Given
    let email = ""
    let password = ""

    // When
    sut.onError = { errors in
      // Then
      XCTAssertEqual(errors.first!.localizedDescription, ValidationError.empty(.email).localizedDescription) 
     // โ˜๏ธ Changed
    }
    sut.login(email: email, password: password)
  }

Build & Test, and now the green checkmarks are back!

Now let's look at our login method inside the LoginViewModel

Demo

There is a lot to improve here, for starters, it's not complying with the Single Responsibility Principle, it has side effects and it can definitely be broken down because it's not just logging the user in, it's validating and checking before actually logging in

So, how about we start by breaking it down into something smaller and focused?

Let's do so, only this time, I'll be recording some quick videos since writing might not be the most digestible way in demo-ing this.

Conclusion

Phew First time doing live coding and boy that was stressful editing

Well, at least it's done now and what matters the most is delivering benefit to whoever reads or watches this video and the demo, I hope that the concepts explained here are a bit easier to understand, or hopefully you did understand them which is a feat on its own!

If not, don't worry, no matter how much you feel the topic is still far, you are 100% closer than you were before checking this out โœŒ๏ธ

And like always, feel free to check our progress on GitHub from here!

ย