diff --git a/03_Animal/vbnet/Animal.Tests/Animal.Tests.vbproj b/03_Animal/vbnet/Animal.Tests/Animal.Tests.vbproj
index 05d00314..62a341e9 100644
--- a/03_Animal/vbnet/Animal.Tests/Animal.Tests.vbproj
+++ b/03_Animal/vbnet/Animal.Tests/Animal.Tests.vbproj
@@ -3,12 +3,12 @@
Animal.Tests
net6.0
-
false
+ On
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -20,4 +20,8 @@
+
+
+
+
diff --git a/03_Animal/vbnet/Animal.Tests/MockConsole.vb b/03_Animal/vbnet/Animal.Tests/MockConsole.vb
new file mode 100644
index 00000000..a78dc50f
--- /dev/null
+++ b/03_Animal/vbnet/Animal.Tests/MockConsole.vb
@@ -0,0 +1,65 @@
+Imports System.IO
+
+Public Class MockConsole
+ Inherits ConsoleAdapterBase
+
+ Private inputs As Queue(Of String)
+ Public ReadOnly Lines As New List(Of (line As String, centered As Boolean)) From {
+ ("", False)
+ }
+
+ ' TODO it's possible to clear all the lines, and we'd have to check once again in WriteString and WriteCenteredLine if there are any lines
+
+ Sub New(Inputs As IEnumerable(Of String))
+ Me.inputs = New Queue(Of String)(Inputs)
+ End Sub
+
+ Private Sub CheckLinesInitialized()
+ If Lines.Count = 0 Then Lines.Add(("", False))
+ End Sub
+
+ Private Sub WriteString(s As String, Optional centered As Boolean = False)
+ If s Is Nothing Then Return
+ CheckLinesInitialized()
+ s.Split(Environment.NewLine).ForEach(Sub(line, index)
+ If index = 0 Then
+ Dim currentLast = Lines(Lines.Count - 1)
+ ' centered should never come from the current last line
+ ' if WriteCenteredLine is called, it immediately creates a new line
+ Lines(Lines.Count - 1) = (currentLast.line + line, centered)
+ Else
+ Lines.Add((line, centered))
+ End If
+ End Sub)
+ End Sub
+
+ Public Overrides Sub Write(value As Object)
+ WriteString(value?.ToString)
+ End Sub
+
+ Public Overrides Sub WriteLine(value As Object)
+ WriteString(value?.ToString)
+ WriteLine()
+ End Sub
+
+ Public Overrides Sub WriteLine()
+ Lines.Add(("", False))
+ End Sub
+
+ Public Overrides Sub WriteCenteredLine(value As Object)
+ If Lines.Count = 0 Then Lines.Add(("", False))
+ Dim currentLast = Lines(Lines.Count - 1).line
+ If currentLast.Length > 0 Then Throw New InvalidOperationException("Can only write centered line if cursor is at start of line.")
+ WriteString(value?.ToString, True)
+ WriteLine()
+ End Sub
+
+ Public Overrides Function ReadLine() As String
+ ' Indicates the end of a test run, for programs which loop endlessly
+ If inputs.Count = 0 Then Throw New EndOfStreamException("End of inputs")
+
+ Dim nextInput = inputs.Dequeue.Trim
+ WriteLine(nextInput)
+ Return nextInput
+ End Function
+End Class
diff --git a/03_Animal/vbnet/Animal.Tests/TestContainer.vb b/03_Animal/vbnet/Animal.Tests/TestContainer.vb
new file mode 100644
index 00000000..3aed862a
--- /dev/null
+++ b/03_Animal/vbnet/Animal.Tests/TestContainer.vb
@@ -0,0 +1,80 @@
+Imports Xunit
+Imports Animal
+Imports System.IO
+
+Public Class TestContainer
+ Private Shared Function ResponseVariantExpander(src As IEnumerable(Of String)) As TheoryData(Of String)
+ Dim theoryData = New TheoryData(Of String)
+ src.
+ SelectMany(Function(x) {x, x.Substring(0, 1)}).
+ SelectMany(Function(x) {
+ x,
+ x.ToUpperInvariant,
+ x.ToLowerInvariant,
+ x.ToTitleCase,
+ x.ToReverseCase
+ }).
+ Distinct.
+ ForEach(Sub(x) theoryData.Add(x))
+ Return theoryData
+ End Function
+ Private Shared YesVariantsThepryData As TheoryData(Of String) = ResponseVariantExpander({"yes", "true", "1"})
+ Private Shared Function YesVariants() As TheoryData(Of String)
+ Return YesVariantsThepryData
+ End Function
+ Private Shared NoVariantsThepryData As TheoryData(Of String) = ResponseVariantExpander({"no", "false", "0"})
+ Private Shared Function NoVariants() As TheoryData(Of String)
+ Return NoVariantsThepryData
+ End Function
+
+ ''' Test LIST variants
+
+
+
+
+
+ Sub List(listResponse As String)
+ Dim console As New MockConsole({listResponse})
+ Dim game As New Game(console)
+ Assert.Throws(Of EndOfStreamException)(Sub() game.BeginLoop())
+ Assert.Equal(
+ {
+ "ANIMALS I ALREADY KNOW ARE:",
+ "FISH BIRD "
+ },
+ console.Lines.Slice(-4, -2).Select(Function(x) x.line)
+ )
+ End Sub
+
+ '' Test YES variants
+
+
+ Sub YesVariant(yesVariant As String)
+ Dim console As New MockConsole({yesVariant})
+ Dim game As New Game(console)
+ Assert.Throws(Of EndOfStreamException)(Sub() game.BeginLoop())
+ Assert.Equal(
+ {
+ $"ARE YOU THINKING OF AN ANIMAL? {yesVariant}",
+ "DOES IT SWIM? "
+ },
+ console.Lines.Slice(-2, 0).Select(Function(x) x.line)
+ )
+ End Sub
+
+ '' Test NO variants
+
+
+ Sub NoVariant(noVariant As String)
+ Dim console As New MockConsole({"y", noVariant})
+ Dim game As New Game(console)
+ Assert.Throws(Of EndOfStreamException)(Sub() game.BeginLoop())
+ Assert.Equal(
+ {
+ $"DOES IT SWIM? {noVariant}",
+ "IS IT A BIRD? "
+ },
+ console.Lines.Slice(-2, 0).Select(Function(x) x.line)
+ )
+ End Sub
+End Class
diff --git a/03_Animal/vbnet/Animal.Tests/UnitTest1.vb b/03_Animal/vbnet/Animal.Tests/UnitTest1.vb
deleted file mode 100644
index fe32490e..00000000
--- a/03_Animal/vbnet/Animal.Tests/UnitTest1.vb
+++ /dev/null
@@ -1,12 +0,0 @@
-Imports System
-Imports Xunit
-
-Namespace Animal.Tests
- Public Class UnitTest1
-
- Sub TestSub()
-
- End Sub
- End Class
-End Namespace
-
diff --git a/03_Animal/vbnet/Animal/Shared/ConsoleAdapter.vb b/03_Animal/vbnet/Animal/Shared/ConsoleAdapter.vb
index 132f1968..f49394e0 100644
--- a/03_Animal/vbnet/Animal/Shared/ConsoleAdapter.vb
+++ b/03_Animal/vbnet/Animal/Shared/ConsoleAdapter.vb
@@ -14,6 +14,7 @@
End Sub
Public Overrides Sub WriteCenteredLine(value As Object)
+ If Console.CursorLeft <> 0 Then Throw New InvalidOperationException("Can only write centered line if cursor is at start of line.")
Dim toWrite = If(value?.ToString, "")
Console.WriteLine($"{Space((Console.WindowWidth - toWrite.Length) \ 2)}{toWrite}")
End Sub
diff --git a/03_Animal/vbnet/Animal/Shared/Extensions.vb b/03_Animal/vbnet/Animal/Shared/Extensions.vb
index 83d1914b..e10df441 100644
--- a/03_Animal/vbnet/Animal/Shared/Extensions.vb
+++ b/03_Animal/vbnet/Animal/Shared/Extensions.vb
@@ -1,6 +1,11 @@
Imports System.Runtime.CompilerServices
Public Module Extensions
+ Public Sub ForEach(Of T)(src As IEnumerable(Of T), action As Action(Of T))
+ For Each x In src
+ action(x)
+ Next
+ End Sub
Public Sub ForEach(Of T)(src As IEnumerable(Of T), action As Action(Of T, Integer))
Dim index As Integer
For Each x In src
@@ -18,4 +23,28 @@ Public Module Extensions
If Not s.EndsWith(toAppend, StringComparison.OrdinalIgnoreCase) Then s += toAppend
Return s
End Function
+
+ Public Function ToTitleCase(s As String) As String
+ If s Is Nothing Then Return Nothing
+ Return Char.ToUpperInvariant(s(0)) + s.Substring(1).ToUpperInvariant
+ End Function
+
+ ' https://stackoverflow.com/a/3681580/111794
+ Public Function ToReverseCase(s As String) As String
+ If s Is Nothing Then Return Nothing
+ Return New String(s.Select(Function(c) If(
+ Not Char.IsLetter(c),
+ c,
+ If(
+ Char.IsUpper(c), Char.ToLowerInvariant(c), Char.ToUpperInvariant(c)
+ )
+ )).ToArray)
+ End Function
+
+ ' https://stackoverflow.com/a/58132204/111794
+ Public Function Slice(Of T)(lst As IList(Of T), start As Integer, [end] As Integer) As T()
+ start = If(start >= 0, start, lst.Count + start)
+ [end] = If([end] > 0, [end], lst.Count + [end])
+ Return lst.Skip(start).Take([end] - start).ToArray
+ End Function
End Module