GithubHelp home page GithubHelp logo

sebaoliveri / fluent-assertions Goto Github PK

View Code? Open in Web Editor NEW
4.0 4.0 0.0 301 KB

fluent (composable) assertions in Scala for describing domain validations

License: MIT License

Scala 100.00%
assertions fluent-assertions ddd-validation ddd tdd scala unit-test

fluent-assertions's Introduction

Fluent assertions

A long long time ago I was taught real devs do not use IFs. However, when writing a condition using objects of different domains there's no way to avoid IFs. In OO preconditions are used when creating new instances to make sure objects are valid since momento zero, as well as describing preconditions when evaluation an object method to make sure invariants are preserved.

Large IF-ELSE lines of codes depend on the developer to maintain and are really hard to reason about. Let's delegate that task to the objects of fluent-assertions and lighten the work of the developer. At the end software models must be legible, representative of what they are modelling.

fluent-assertions is the result of materializing my motivation to propose a validation model to reify assertions as first class objects.

Installation

This lib supports Scala 2.13 Add in your build.sbt the following lines:

resolvers += Resolver.bintrayRepo("fluent-assertions", "releases")
libraryDependencies += "nulluncertainty" %% "fluent-assertions" % "2.0.1"

Usages

A user might sign up using its email or its mobile as part of its credentials:

    case class UserRegistrationForm(maybeEmail: Option[String], maybePhoneNumber: Option[String], password: String)

Now, let's write an assertion to describe the required preconditions to Sign Up a user:

    import org.nulluncertainty.assertion.AssertionBuilder._

    val eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword =
      assertThat({userRegistrationForm:UserRegistrationForm => userRegistrationForm.maybeEmail})
          .isDefined
          .isEmail
        .orThat({userRegistrationForm:UserRegistrationForm => userRegistrationForm.maybePhoneNumber})
          .isDefined
          .isNumber
          .isShorterThan(20)
        .otherwise("Any of the email or the phone number must be specified")
      .and(
        assertThat({userRegistrationForm:UserRegistrationForm => userRegistrationForm.password})
          .isNotBlank
          .isLongerThan(5)
          .isShorterThanOrEqualTo(15)
          .otherwise("The password must be longer than 5 and shorter than 15"))

eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword is an Assertion instance. Underlying it is a composition of multiple assertions.

This assertion could be written more expressive if we parametrized T:

    import org.nulluncertainty.assertion.AssertionBuilder._

    val eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword =
      assertThat[UserRegistrationForm](_.maybeEmail)
          .isDefined
          .isEmail
        .orThat(_.maybePhoneNumber)
          .isDefined
          .isNumber
          .isShorterThan(20)
        .otherwise("Any of the email or the phone number must be specified")
      .and(
        assertThat[UserRegistrationForm](_.password)
          .isNotBlank
          .isLongerThan(5)
          .isShorterThanOrEqualTo(15)
          .otherwise("The password must be longer than 5 and shorter than 15"))

Given:

    val userRegistrationForm = UserRegistrationForm(Some("[email protected]"), None, "1a2b3c$")

eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword requires a context for evalution. The context is an instance of UserRegistrationForm

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .matches {
        case AssertionSuccessfulResult(userRegistrationForm) => // keep going
        case AssertionFailureResult(errors) => // maybe return BadRequest
      }

AssertionFailureResult(errors) reifies the concept of a failure, and groups all the errors cause by unsatisfied assertions.

We can also select another strategy and raise an exception of type AssertionFailureException which also collects all the error messages of the failed assertions after evaluation:

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .signalIfFailed()

If we need a custom exception:

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .signalIfFailed(errors => new DomainValidationException(errors.mkString(", ")))

If you need a type Try:

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .toTry()
      .map(context => /* keep going */)
      .recover { case AssertionFailureException(errors) => /* keep going */ }

or if you need a type Either:

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .toEither()
      .map(context => /* keep going */)
      .recover { case AssertionFailureException(errors) => /* keep going */ }

or if you want to fold over an assertion result to return another type:

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .fold(errorsHandlingFunction, successFunction)
      

You can also use an assertion for testing.

    eitherAnEmailOrAPhoneMustBeSpecifiedAlongWithPassword
      .evaluate(userRegistrationForm)
      .expectsToBeTrue()
      

So far we saw assertions evaluated passing in a context. But we can also write assertions using values instead of functions. In this case we assert UserRegistrationForm instantiation to be valid, otherwise raise an exception. Take a look at the evaluate method. No context is passed in:

    import org.nulluncertainty.assertion.AssertionBuilder._
    
    case class UserRegistrationForm(maybeEmail: Option[String], maybePhoneNumber: Option[String], password: String) {
    
      assertThat(maybeEmail)
          .isDefined
          .isEmail
        .orThat(maybePhoneNumber)
          .isDefined
          .isNumber
          .isShorterThan(20)
        .otherwise("Any of the email or the phone number must be specified")
      .and(
        assertThat(password)
          .isNotBlank
          .isLongerThan(5)
          .isShorterThanOrEqualTo(15)
          .otherwise("The password must be longer than 5 and shorter than 15"))
       .evaluate()
       .signalIfFailed()
    }

So far we saw the example of UserRegistrationForm assertion. But we can build composable assertions using any other types number string date collection option boolean

Assertions be be composed with operators:

ifTrue : When fails returns only anAssertion failure message. When success, evaluates anotherAssertion and returns its result.

    anAssertion.ifTrue(anotherAssertion).evaluate(context)
      

and:

    anAssertion.and(anotherAssertion).evaluate(context)
    

or:

    anAssertion.or(anotherAssertion).evaluate(context)
    

ifFalse:

    anAssertion.ifFalse(anotherAssertion).evaluate(context)
      

ifTrue ifFalse

    anAssertion.ifTrue(anotherAssertion).ifFalse(yetAnotherAssertion).evaluate(context)
      

thenElse (takes a boolean expression as predicate)

    import org.nulluncertainty.extension._    

    "jony".startsWithExp("j").thenElse(anAssertion, anotherAssertion).evaluate(context)
      

Assertions can also be composable by using Map and FlatMap:

    import org.nulluncertainty.extension.QuantifiableExt._

    assertThat({customersByAge: Map[Int,String] => customersByAge}).containsNoDuplicates.otherwise("repeated customers not allowed")
      .map(_.keys)
      .flatMap(ages => assertThat(ages).forAll(age => age.isGreaterThanExp(18)).otherwise("all customers must be 18 years old or greater"))
      .evaluate(Map(
        38 -> "Sebastian",
        40 -> "Juan"
      )).matches {
        case AssertionSuccessfulResult(_) =>
        case AssertionFailureResult(errors) =>
      }

fluent-assertions's People

Contributors

sebaoliveri avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.