test: add unit tests for API response, board, color, game state, move, piece, and square #3

Merged
Janis merged 2 commits from test/api-testing into main 2026-03-25 08:19:43 +01:00
11 changed files with 498 additions and 2 deletions
+2 -1
View File
@@ -49,9 +49,10 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
@@ -0,0 +1,102 @@
package de.nowchess.api.board
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class BoardTest extends AnyFunSuite with Matchers:
private val e2 = Square(File.E, Rank.R2)
private val e4 = Square(File.E, Rank.R4)
private val d7 = Square(File.D, Rank.R7)
test("pieceAt returns Some for occupied square") {
Board.initial.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
}
test("pieceAt returns None for empty square") {
Board.initial.pieceAt(e4) shouldBe None
}
test("withMove moves piece and vacates origin") {
val (board, captured) = Board.initial.withMove(e2, e4)
captured shouldBe None
board.pieceAt(e4) shouldBe Some(Piece.WhitePawn)
board.pieceAt(e2) shouldBe None
}
test("withMove returns captured piece when destination is occupied") {
val from = Square(File.A, Rank.R1)
val to = Square(File.A, Rank.R8)
val b = Board(Map(from -> Piece.WhiteRook, to -> Piece.BlackRook))
val (board, captured) = b.withMove(from, to)
captured shouldBe Some(Piece.BlackRook)
board.pieceAt(to) shouldBe Some(Piece.WhiteRook)
board.pieceAt(from) shouldBe None
}
test("pieces returns the underlying map") {
val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map)
b.pieces shouldBe map
}
test("Board.apply constructs board from map") {
val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map)
b.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
}
test("initial board has 32 pieces") {
Board.initial.pieces should have size 32
}
test("initial board has 16 white pieces") {
Board.initial.pieces.values.count(_.color == Color.White) shouldBe 16
}
test("initial board has 16 black pieces") {
Board.initial.pieces.values.count(_.color == Color.Black) shouldBe 16
}
test("initial board white pawns on rank 2") {
File.values.foreach { file =>
Board.initial.pieceAt(Square(file, Rank.R2)) shouldBe Some(Piece.WhitePawn)
}
}
test("initial board black pawns on rank 7") {
File.values.foreach { file =>
Board.initial.pieceAt(Square(file, Rank.R7)) shouldBe Some(Piece.BlackPawn)
}
}
test("initial board white back rank") {
val expectedBackRank = Vector(
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
)
File.values.zipWithIndex.foreach { (file, i) =>
Board.initial.pieceAt(Square(file, Rank.R1)) shouldBe
Some(Piece(Color.White, expectedBackRank(i)))
}
}
test("initial board black back rank") {
val expectedBackRank = Vector(
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
)
File.values.zipWithIndex.foreach { (file, i) =>
Board.initial.pieceAt(Square(file, Rank.R8)) shouldBe
Some(Piece(Color.Black, expectedBackRank(i)))
}
}
test("ranks 3-6 are empty on initial board") {
val emptyRanks = Seq(Rank.R3, Rank.R4, Rank.R5, Rank.R6)
for
rank <- emptyRanks
file <- File.values
do
Board.initial.pieceAt(Square(file, rank)) shouldBe None
}
@@ -0,0 +1,22 @@
package de.nowchess.api.board
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class ColorTest extends AnyFunSuite with Matchers:
test("White.opposite returns Black") {
Color.White.opposite shouldBe Color.Black
}
test("Black.opposite returns White") {
Color.Black.opposite shouldBe Color.White
}
test("White.label returns 'White'") {
Color.White.label shouldBe "White"
}
test("Black.label returns 'Black'") {
Color.Black.label shouldBe "Black"
}
@@ -0,0 +1,60 @@
package de.nowchess.api.board
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class PieceTest extends AnyFunSuite with Matchers:
test("Piece holds color and pieceType") {
val p = Piece(Color.White, PieceType.Queen)
p.color shouldBe Color.White
p.pieceType shouldBe PieceType.Queen
}
test("WhitePawn convenience constant") {
Piece.WhitePawn shouldBe Piece(Color.White, PieceType.Pawn)
}
test("WhiteKnight convenience constant") {
Piece.WhiteKnight shouldBe Piece(Color.White, PieceType.Knight)
}
test("WhiteBishop convenience constant") {
Piece.WhiteBishop shouldBe Piece(Color.White, PieceType.Bishop)
}
test("WhiteRook convenience constant") {
Piece.WhiteRook shouldBe Piece(Color.White, PieceType.Rook)
}
test("WhiteQueen convenience constant") {
Piece.WhiteQueen shouldBe Piece(Color.White, PieceType.Queen)
}
test("WhiteKing convenience constant") {
Piece.WhiteKing shouldBe Piece(Color.White, PieceType.King)
}
test("BlackPawn convenience constant") {
Piece.BlackPawn shouldBe Piece(Color.Black, PieceType.Pawn)
}
test("BlackKnight convenience constant") {
Piece.BlackKnight shouldBe Piece(Color.Black, PieceType.Knight)
}
test("BlackBishop convenience constant") {
Piece.BlackBishop shouldBe Piece(Color.Black, PieceType.Bishop)
}
test("BlackRook convenience constant") {
Piece.BlackRook shouldBe Piece(Color.Black, PieceType.Rook)
}
test("BlackQueen convenience constant") {
Piece.BlackQueen shouldBe Piece(Color.Black, PieceType.Queen)
}
test("BlackKing convenience constant") {
Piece.BlackKing shouldBe Piece(Color.Black, PieceType.King)
}
@@ -0,0 +1,30 @@
package de.nowchess.api.board
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class PieceTypeTest extends AnyFunSuite with Matchers:
test("Pawn.label returns 'Pawn'") {
PieceType.Pawn.label shouldBe "Pawn"
}
test("Knight.label returns 'Knight'") {
PieceType.Knight.label shouldBe "Knight"
}
test("Bishop.label returns 'Bishop'") {
PieceType.Bishop.label shouldBe "Bishop"
}
test("Rook.label returns 'Rook'") {
PieceType.Rook.label shouldBe "Rook"
}
test("Queen.label returns 'Queen'") {
PieceType.Queen.label shouldBe "Queen"
}
test("King.label returns 'King'") {
PieceType.King.label shouldBe "King"
}
@@ -0,0 +1,62 @@
package de.nowchess.api.board
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class SquareTest extends AnyFunSuite with Matchers:
test("Square.toString produces lowercase file and rank number") {
Square(File.E, Rank.R4).toString shouldBe "e4"
}
test("Square.toString for a1") {
Square(File.A, Rank.R1).toString shouldBe "a1"
}
test("Square.toString for h8") {
Square(File.H, Rank.R8).toString shouldBe "h8"
}
test("fromAlgebraic parses valid square e4") {
Square.fromAlgebraic("e4") shouldBe Some(Square(File.E, Rank.R4))
}
test("fromAlgebraic parses valid square a1") {
Square.fromAlgebraic("a1") shouldBe Some(Square(File.A, Rank.R1))
}
test("fromAlgebraic parses valid square h8") {
Square.fromAlgebraic("h8") shouldBe Some(Square(File.H, Rank.R8))
}
test("fromAlgebraic is case-insensitive for file") {
Square.fromAlgebraic("E4") shouldBe Some(Square(File.E, Rank.R4))
}
test("fromAlgebraic returns None for empty string") {
Square.fromAlgebraic("") shouldBe None
}
test("fromAlgebraic returns None for string too short") {
Square.fromAlgebraic("e") shouldBe None
}
test("fromAlgebraic returns None for string too long") {
Square.fromAlgebraic("e42") shouldBe None
}
test("fromAlgebraic returns None for invalid file character") {
Square.fromAlgebraic("z4") shouldBe None
}
test("fromAlgebraic returns None for non-digit rank") {
Square.fromAlgebraic("ex") shouldBe None
}
test("fromAlgebraic returns None for rank 0") {
Square.fromAlgebraic("e0") shouldBe None
}
test("fromAlgebraic returns None for rank 9") {
Square.fromAlgebraic("e9") shouldBe None
}
@@ -0,0 +1,77 @@
package de.nowchess.api.game
import de.nowchess.api.board.Color
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class GameStateTest extends AnyFunSuite with Matchers:
test("CastlingRights.None has both flags false") {
CastlingRights.None.kingSide shouldBe false
CastlingRights.None.queenSide shouldBe false
}
test("CastlingRights.Both has both flags true") {
CastlingRights.Both.kingSide shouldBe true
CastlingRights.Both.queenSide shouldBe true
}
test("CastlingRights constructor sets fields") {
val cr = CastlingRights(kingSide = true, queenSide = false)
cr.kingSide shouldBe true
cr.queenSide shouldBe false
}
test("GameResult cases exist") {
GameResult.WhiteWins shouldBe GameResult.WhiteWins
GameResult.BlackWins shouldBe GameResult.BlackWins
GameResult.Draw shouldBe GameResult.Draw
}
test("GameStatus.NotStarted") {
GameStatus.NotStarted shouldBe GameStatus.NotStarted
}
test("GameStatus.InProgress") {
GameStatus.InProgress shouldBe GameStatus.InProgress
}
test("GameStatus.Finished carries result") {
val status = GameStatus.Finished(GameResult.Draw)
status shouldBe GameStatus.Finished(GameResult.Draw)
status match
case GameStatus.Finished(r) => r shouldBe GameResult.Draw
case _ => fail("expected Finished")
}
test("GameState.initial has standard FEN piece placement") {
GameState.initial.piecePlacement shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
}
test("GameState.initial active color is White") {
GameState.initial.activeColor shouldBe Color.White
}
test("GameState.initial white has full castling rights") {
GameState.initial.castlingWhite shouldBe CastlingRights.Both
}
test("GameState.initial black has full castling rights") {
GameState.initial.castlingBlack shouldBe CastlingRights.Both
}
test("GameState.initial en-passant target is None") {
GameState.initial.enPassantTarget shouldBe None
}
test("GameState.initial half-move clock is 0") {
GameState.initial.halfMoveClock shouldBe 0
}
test("GameState.initial full-move number is 1") {
GameState.initial.fullMoveNumber shouldBe 1
}
test("GameState.initial status is InProgress") {
GameState.initial.status shouldBe GameStatus.InProgress
}
@@ -0,0 +1,56 @@
package de.nowchess.api.move
import de.nowchess.api.board.{File, Rank, Square}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class MoveTest extends AnyFunSuite with Matchers:
private val e2 = Square(File.E, Rank.R2)
private val e4 = Square(File.E, Rank.R4)
test("Move defaults moveType to Normal") {
val m = Move(e2, e4)
m.moveType shouldBe MoveType.Normal
}
test("Move stores from and to squares") {
val m = Move(e2, e4)
m.from shouldBe e2
m.to shouldBe e4
}
test("Move with CastleKingside moveType") {
val m = Move(e2, e4, MoveType.CastleKingside)
m.moveType shouldBe MoveType.CastleKingside
}
test("Move with CastleQueenside moveType") {
val m = Move(e2, e4, MoveType.CastleQueenside)
m.moveType shouldBe MoveType.CastleQueenside
}
test("Move with EnPassant moveType") {
val m = Move(e2, e4, MoveType.EnPassant)
m.moveType shouldBe MoveType.EnPassant
}
test("Move with Promotion to Queen") {
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Queen))
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
}
test("Move with Promotion to Knight") {
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Knight))
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Knight)
}
test("Move with Promotion to Bishop") {
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Bishop))
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Bishop)
}
test("Move with Promotion to Rook") {
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Rook))
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Rook)
}
@@ -0,0 +1,23 @@
package de.nowchess.api.player
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class PlayerInfoTest extends AnyFunSuite with Matchers:
test("PlayerId.apply wraps a string") {
val id = PlayerId("player-123")
id.value shouldBe "player-123"
}
test("PlayerId.value unwraps to original string") {
val raw = "abc-456"
PlayerId(raw).value shouldBe raw
}
test("PlayerInfo holds id and displayName") {
val id = PlayerId("p1")
val info = PlayerInfo(id, "Magnus")
info.id.value shouldBe "p1"
info.displayName shouldBe "Magnus"
}
@@ -0,0 +1,62 @@
package de.nowchess.api.response
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class ApiResponseTest extends AnyFunSuite with Matchers:
test("ApiResponse.Success carries data") {
val r = ApiResponse.Success(42)
r.data shouldBe 42
}
test("ApiResponse.Failure carries error list") {
val err = ApiError("CODE", "msg")
val r = ApiResponse.Failure(List(err))
r.errors shouldBe List(err)
}
test("ApiResponse.error creates single-error Failure") {
val err = ApiError("NOT_FOUND", "not found")
val f = ApiResponse.error(err)
f shouldBe ApiResponse.Failure(List(err))
}
test("ApiError holds code and message") {
val e = ApiError("CODE", "message")
e.code shouldBe "CODE"
e.message shouldBe "message"
e.field shouldBe None
}
test("ApiError holds optional field") {
val e = ApiError("INVALID", "bad value", Some("email"))
e.field shouldBe Some("email")
}
test("Pagination.totalPages with exact division") {
Pagination(page = 0, pageSize = 10, totalItems = 30).totalPages shouldBe 3
}
test("Pagination.totalPages rounds up") {
Pagination(page = 0, pageSize = 10, totalItems = 25).totalPages shouldBe 3
}
test("Pagination.totalPages is 0 when totalItems is 0") {
Pagination(page = 0, pageSize = 10, totalItems = 0).totalPages shouldBe 0
}
test("Pagination.totalPages is 0 when pageSize is 0") {
Pagination(page = 0, pageSize = 0, totalItems = 100).totalPages shouldBe 0
}
test("Pagination.totalPages is 0 when pageSize is negative") {
Pagination(page = 0, pageSize = -1, totalItems = 100).totalPages shouldBe 0
}
test("PagedResponse holds items and pagination") {
val pagination = Pagination(page = 1, pageSize = 5, totalItems = 20)
val pr = PagedResponse(List("a", "b"), pagination)
pr.items shouldBe List("a", "b")
pr.pagination shouldBe pagination
}
+2 -1
View File
@@ -52,9 +52,10 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {