DEV Community

Cover image for From PHP to Kotlin - Day 6 - Entities
Fernando Aparicio
Fernando Aparicio

Posted on • Updated on

From PHP to Kotlin - Day 6 - Entities

Entities are other thing. Are more complex and require more care.

Specifications
Mush have unique identifier
Must have value objects
Must be able to contain other entities
Must be able to be constructed in different ways if necessary "named constructors"

In this case we're going to create the Mower Entity.

The truth is that before starting this activity, the Mower entity was de first that was created, but it was quite anemic, since it needed other value objects like position or orientation to be created by composition and that's why I left it parked, since that didn't serve our purpose.

What we need from our Mower is:

  • That can be build
  • That can turn left
  • That can turn right
  • That can move along
  • That detects if it goes out of the limits of the map

Which leaves us with a test like this:

private const val MOVE_LEFT: String = "L"
private const val MOVE_RIGHT: String = "R"
private const val MOVE_FORWARD: String = "F"

private const val X_POSITION: Int = 1
private const val Y_POSITION: Int = 1
private const val STEP: Int = 1

private const val NORTH_ORIENTATION: String = "N"
private const val WEST_ORIENTATION: String = "W"
private const val EAST_ORIENTATION: String = "E"
private const val SOUTH_ORIENTATION: String = "S"

internal class MowerTest {
    private lateinit var mower: Mower
    private val mowerId: MowerId = MowerId.build(UUID.randomUUID().toString())
    private lateinit var mowerPosition: MowerPosition

    @BeforeEach
    fun setUp() {
        mowerPosition = MowerPosition.build(
            XMowerPosition.build(X_POSITION),
            YMowerPosition.build(Y_POSITION),
            MowerOrientation.build(NORTH_ORIENTATION)
        )

        mower = Mower.build(mowerId, mowerPosition)
    }

    @Test
    fun `Should be build`() {
        val mower = Mower.build(mowerId, mowerPosition)

        assertThat(mower).isInstanceOf(Mower::class.java)
        assertThat(mower.mowerId).isEqualTo(mowerId)
        assertThat(mower.mowerPosition()).isEqualTo(mowerPosition)
    }

    @Test
    fun `Should be able to turn left`() {
        val expectedMowerOrientation = MowerOrientation.build(WEST_ORIENTATION)

        mower.move(MowerMovement.build(MOVE_LEFT))
        assertThat(mower.mowerPosition().orientation).isEqualTo(expectedMowerOrientation)
    }

    @Test
    fun `Should be able to turn right`() {
        val expectedMowerOrientation = MowerOrientation.build(EAST_ORIENTATION)

        mower.move(MowerMovement.build(MOVE_RIGHT))
        assertThat(mower.mowerPosition().orientation).isEqualTo(expectedMowerOrientation)
    }

    @Test
    fun `Should be able to move forward`() {
        mower.move(MowerMovement.build(MOVE_FORWARD))
        assertThat(mower.mowerPosition().xPosition.value).isEqualTo(X_POSITION)
        assertThat(mower.mowerPosition().yPosition.value).isEqualTo(Y_POSITION + STEP)
    }

    @ParameterizedTest
    @MethodSource("positionAndMovementProvider")
    fun `Should throw exception if goes out of bounds`(xPosition: Int, yPosition: Int, orientation: String) {
        val mowerPosition = MowerPosition.build(
            XMowerPosition.build(xPosition),
            YMowerPosition.build(yPosition),
            MowerOrientation.build(orientation)
        )

        val mower = Mower.build(mowerId, mowerPosition)

        assertThrows(InvalidMowerPositionException::class.java) {
            mower.move(MowerMovement.build(MOVE_FORWARD))
        }
    }

    companion object {
        @JvmStatic
        fun positionAndMovementProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments(0, 0, SOUTH_ORIENTATION),
                Arguments.arguments(0, 0, WEST_ORIENTATION),
                Arguments.arguments(0, 5, NORTH_ORIENTATION),
                Arguments.arguments(5, 0, EAST_ORIENTATION)
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If all went well in the previous value objects, our Mower should look like this:

import mower.mower.value_object.MowerId
import mower.mower.value_object.MowerMovement
import mower.mower.value_object.MowerPosition

class Mower private constructor(val mowerId: MowerId, private var mowerPosition: MowerPosition) {

    fun move(movement: MowerMovement) {
        mowerPosition = mowerPosition.move(movement)
    }

    companion object {
        @JvmStatic
        fun build(mowerId: MowerId, mowerPosition: MowerPosition): Mower {
            return Mower(mowerId, mowerPosition)
        }
    }

    fun mowerPosition(): MowerPosition {
        return mowerPosition
    }
}
Enter fullscreen mode Exit fullscreen mode

All the logic has been distributed in the value objects in a natural way. Every piece fulfills its function. Te Mower sould only apply the movement that it passes to the MowrePosition object, which decides if it's a forward movement or a change of direction, and executes the necessary changes...

The Value objects XMowerPosition and YMowerPosition watch for negative positions in the coordinates. But there aren't able to know if they are outside the map in positive coordinates. The tests that we've created for Mower will fail in this specific field, so we must pass the map to it to validate that casuistry...


Bonus test and context

The truth is that I'm quite a fan of giving a little explanation when the tests are executed. You never know when one is going to fail and the idea is that the person who comes after us has a bit of context as to why is failing.
See a test without context about values used it can be difficult.

...
    @ParameterizedTest(name = "{0}")
    @MethodSource("positionAndMovementProvider")
    fun `Should throw exception if goes out of bounds`(scenario: String, xPosition: Int, yPosition: Int, orientation: String) {
...
    }

    companion object {
        @JvmStatic
        fun positionAndMovementProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("Out of bounds by negative value movement axis Y", 5, 0, SOUTH_ORIENTATION),
                Arguments.arguments("Out of bounds by negative value movement axis X", 0, 5, WEST_ORIENTATION),
                Arguments.arguments("Out of bounds movement axis Y", 0, 5, NORTH_ORIENTATION),
                Arguments.arguments("Out of bounds movement axis X", 5, 0, EAST_ORIENTATION)
            )
        }
    }
Enter fullscreen mode Exit fullscreen mode

It may seem a bit strange to add an argument that is not going to be used in the test.... but it makes sense when...

We go from this:

Context in tests

Or this:

Context in tests

To this:

Context in tests

Much clearer and more readable...

Although JUnit gives us a warning for not actually using the variable... it seems that it doesn't like our use of it.

Context in tests


Links and resources

Repository Mower Kata

Discussion (0)