Monday, 17 August 2020

REST API Move concept with Test

Previously we have been setting up our application and our repo, now lets do some real coding; earlier we created a controller with three main endpoints:
  • randomMove: will make a random tic tac toe move
  • simpleMove: will make a simple move that will try to win the game or block the opponent
  • expertMove: will make the best move possible
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace src.controllers.v1 {

  [ApiController]
  [Route("api/v1/[controller]")]
  public class GameController : ControllerBase {
    private readonly ILogger<GameController_logger;

    public GameController(ILogger<GameControllerlogger){
      this._logger = logger;
    }

    [HttpGet("HelloWorld")]
    public ActionResult<stringHelloWorld() => Ok($"hello world");

    [HttpGet("RandomMove")]
    public ActionResult<stringRandomMove([FromQuerystring[] m){
      return "Random Move";
    }

    [HttpGet("SimpleMove")]
    public ActionResult<stringSimpleMove([FromQuerystring[] m){
      return "Simple Move";
    }

    [HttpGet("ExpertMove")]
    public ActionResult<stringExpertMove([FromQuerystring[] m){
      return "Expert Move";
    }
  }
}


We decided that we where going to submit our game as a series of moves using postman something along the lines of {{url}}{{version}}/game/RandomMove?m=4x&m=0o&m=8x&m=5o so we have this concept of a Game as a series of moves. It makes sense that we create a move class, however the idea of sending a number (0 to 8) representing our grid location is going to be a pain in the arse when it comes to our game logic; thus we'll convert our inputs form "#X" or "Xo" to a symbol with corresponding x and y coordinates.

lets start with an interface for our move class, but first lets create a models folder in our app, and then a sub folder for interfaces, in those folder place a move.cs file into the models folder and an IMove.cs file in the interface folder.


with the above ready lets start with our interface. open the IMove.cs file; we first start with creating our namespace and then adding our required properties.

namespace tictactoe.game.interfaces{
  public interface IMove{
    char Symbol {getset;}
   (int Xint YCoordinates {getset;}
  }
}


Notice that our namespace doesn't match our folder structure, this is a matter of personal preference, some devs will put all of there interfaces into an interface folder at the root of the project or they will put it into the models folder, or some will define both the interface and the class in the same file, whereas there are best practices, in my experience the only thing that matters is consistency, so pick what works for you and your team and stick to it.

Now the "Symbol" property is pretty straight forward so lets not talk about it but lets look at the "Coordinates" property, our type is actually a named tuple that is a combination of types in our case two integers representing our x and y coordinates.

Next lets implement this interface in our move class

using tictactoe.game.interfaces;
using System;
using System.Text.RegularExpressions;

namespace tictactoe.game.models
{
  public class Move : IMove
  {
    private char _symbol;
    public char Symbol { 
      get => _symbol
      set {
        var v = Char.ToLower(value);
        if(v  == 'x' || v == 'o')
          _symbol = v;
        else
          throw new ArgumentOutOfRangeException(nameof(Symbol), value$"The symbol must be an 'x' or an 'o'; the provided value was '{value}'");
        }
      }

    private  (int Xint Y_coordinates;
    public (int Xint YCoordinates { 
      get => _coordinates
      set {
        if(value.X > -1 &&  value.X < 9 && value.Y > -1 &&  value.Y < 9)
          _coordinates = value;
        else
          throw new ArgumentOutOfRangeException(nameof(Coordinates), value$"The {nameof(Coordinates)} value must be an integer between 0 and 8; {value} was provided");
      } 
    }

    public Move(){}

    public Move(string value): this() {
      var regEx = new Regex("^[0-8][xo]$"RegexOptions.IgnoreCase);

      if(regEx.IsMatch(value)) {
        var num = Int32.Parse(value[0].ToString());
        var x = num % 3;
        var y = (int)Math.Floor((double)num / 3); 
        
        this.Coordinates = (x,y);
        this.Symbol = Char.ToLower(value[1]);
      }
      else{
        throw new ArgumentException("The move must be submitted in the format '2x' or '5o'"nameof(value));
      }
    }
  }
}

now as you can see we created some checks in our properties set methods and a couple of constructors, a parameter-less one which will eventually facilitate serialization, it almost always a good idea to have a parameter-less constructor explicitly implemented.

now lets go back to our controller and transform our array of strings into an array of moves.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using tictactoe.game.interfaces;
using tictactoe.game.models;

namespace src.controllers.v1 {

  [ApiController]
  [Route("api/v1/[controller]")]
  public class GameController : ControllerBase {
    private readonly ILogger<GameController_logger;

    public GameController(ILogger<GameControllerlogger){
      this._logger = logger;
    }

    [HttpGet("HelloWorld")]
    public ActionResult<stringHelloWorld() => Ok($"hello world");

    [HttpGet("RandomMove")]
    public ActionResult<stringRandomMove([FromQuerystring[] m){
        IEnumerable<IMovemoves = m.Select(m => new Move(m));
      return "Random Move";
    }

    [HttpGet("SimpleMove")]
    public ActionResult<stringSimpleMove([FromQuerystring[] m){
      IEnumerable<IMovemoves = m.Select(m => new Move(m));
      return "Simple Move";
    }

    [HttpGet("ExpertMove")]
    public ActionResult<stringExpertMove([FromQuerystring[] m){
      IEnumerable<IMovemoves = m.Select(m => new Move(m));
      return "Expert Move";
    }
  }
}

with that complete you are probably thinking, this is going to be a pain to test, I'm going to have to create end points in postman to test all these combinations, to which i would say yes that would suck if we were rookies, but we are not.

cue the bad ass music and lets write some unit tests.
lets start by creating a moveTests.cs file in our test project


next lets write our unit tests, so open up our newly created MoveTests.cs file 

using tictactoe.game.models;
using Xunit;

namespace tictactoe.unitTests
{
    public class MoveTests
    {
        [Fact]
        public void Test_X_Coordinates()
        {
            var coordinates = new [] {
                //first column
                new { input = "0x"expected = new { symbol = 'x'x = 0y = 0 } },
                new { input = "1X"expected = new { symbol = 'x'x = 1y = 0 } },
                new { input = "2x"expected = new { symbol = 'x'x = 2y = 0 } },

                //second column
                new { input = "3x"expected = new { symbol = 'x'x = 0y = 1 } },
                new { input = "4X"expected = new { symbol = 'x'x = 1y = 1 } },
                new { input = "5x"expected = new { symbol = 'x'x = 2y = 1 } },

                //third column
                new { input = "6X"expected = new { symbol = 'x'x = 0y = 2 } },
                new { input = "7x"expected = new { symbol = 'x'x = 1y = 2 } },
                new { input = "8X"expected = new { symbol = 'x'x = 2y = 2 } },
            };

            foreach(var c in coordinates)
            {
                var move = new Move(c.input);

                Assert.Equal<int>(c.expected.xmove.Coordinates.X );
                Assert.Equal<int>(c.expected.ymove.Coordinates.Y);
                Assert.Equal<char>(c.expected.symbolmove.Symbol);
            }
        }
   
         [Fact]
        public void Test_Y_Coordinates()
        {
            var coordinates = new [] {
                //first column
                new { input = "0o"expected = new { symbol = 'o'x = 0y = 0 } },
                new { input = "1o"expected = new { symbol = 'o'x = 1y = 0 } },
                new { input = "2o"expected = new { symbol = 'o'x = 2y = 0 } },

                //second column
                new { input = "3O"expected = new { symbol = 'o'x = 0y = 1 } },
                new { input = "4O"expected = new { symbol = 'o'x = 1y = 1 } },
                new { input = "5o"expected = new { symbol = 'o'x = 2y = 1 } },

                //third column
                new { input = "6O"expected = new { symbol = 'o'x = 0y = 2 } },
                new { input = "7o"expected = new { symbol = 'o'x = 1y = 2 } },
                new { input = "8O"expected = new { symbol = 'o'x = 2y = 2 } },
            };

            foreach(var c in coordinates)
            {
                var move = new Move(c.input);

                Assert.Equal<int>(c.expected.xmove.Coordinates.X );
                Assert.Equal<int>(c.expected.ymove.Coordinates.Y);
                Assert.Equal<char>(c.expected.symbolmove.Symbol);
            }
        }
    }
}

now we could have created separate tests for each combination or we could have created our coordinates arrays using some nested for each loops, I find that this approach is compact enough that you are not looking at too much, but still explicit enough that one can easily tell what is happening without too much effort.

lets execute our tests


and in a matter of seconds we have managed to test our move class, and now forever more we can test our move class with a simple command.

now lets check this sucker in, we wont bother with tagging it because we still have to create our game logic as well as implementing it in our controller.


git add .
git commit -m 'created move logic & tests'
git push

next lets create our game logic