Skip to content

Commit

Permalink
Code Quality: Write tests (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lamparter authored Jan 18, 2025
1 parent a7f0e5d commit 05585c9
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 1 deletion.
26 changes: 26 additions & 0 deletions JsonBinder.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder.Console", "src\Riverside.JsonBinder.Console\Riverside.JsonBinder.Console.csproj", "{A0C21F10-32C7-4D6D-A8F1-022C808A4615}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FDCA6BBE-B3FF-4CF8-AC2F-AEA0F5A2171E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{A9D4D6A1-C565-435B-8CE2-36FDF6D163F8}"
ProjectSection(SolutionItems) = preProject
eng\AdditionalFiles.props = eng\AdditionalFiles.props
eng\CurrentVersion.props = eng\CurrentVersion.props
eng\PackageLogo.png = eng\PackageLogo.png
eng\PackageMetadata.props = eng\PackageMetadata.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder.Tests", "tests\Riverside.JsonBinder.Tests\Riverside.JsonBinder.Tests.csproj", "{4197DF7D-233B-45E6-911F-485B188A6B79}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,8 +35,20 @@ Global
{A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Release|Any CPU.Build.0 = Release|Any CPU
{4197DF7D-233B-45E6-911F-485B188A6B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4197DF7D-233B-45E6-911F-485B188A6B79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4197DF7D-233B-45E6-911F-485B188A6B79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4197DF7D-233B-45E6-911F-485B188A6B79}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0B9E827E-F59A-419F-9BAB-DBB5512627D9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{A0C21F10-32C7-4D6D-A8F1-022C808A4615} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{4197DF7D-233B-45E6-911F-485B188A6B79} = {FDCA6BBE-B3FF-4CF8-AC2F-AEA0F5A2171E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A7D91CF4-FBEE-4762-A037-CFA995F795FC}
EndGlobalSection
EndGlobal
3 changes: 3 additions & 0 deletions src/Riverside.JsonBinder/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Riverside.JsonBinder.Tests")]
2 changes: 1 addition & 1 deletion src/Riverside.JsonBinder/Serialization/PHPSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private void ProcessNode(JsonNode node, string className, List<string> classes)
{
if (node is JsonObject obj)
{
var classDef = $"class {className} {{\n";
var classDef = $"class {className} {{";
foreach (var property in obj)
{
classDef += $"\n public ${property.Key};";
Expand Down
1 change: 1 addition & 0 deletions src/Riverside.JsonBinder/Serialization/RubySerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ private void ProcessNode(JsonNode node, string className, List<string> classes)
{
var classDef = $"class {className}\n attr_accessor ";
classDef += string.Join(", ", obj.Select(property => $":{property.Key}"));
classDef += "\nend";
classes.Add(classDef);

foreach (var property in obj)
Expand Down
90 changes: 90 additions & 0 deletions tests/Riverside.JsonBinder.Tests/JsonSerializerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace Riverside.JsonBinder.Tests;

[TestClass]
public class JsonSerializerTests
{
[TestMethod]
[DataRow("{\"name\":\"John\"}", SerializableLanguage.CSharp, "public class Root\n{\n public string name { get; set; }\n}")]
[DataRow("{\"name\":\"John\"}", SerializableLanguage.Python, "class Root:\n def __init__(self):\n self.name: str = None")]
[DataRow("{\"name\":\"John\"}", SerializableLanguage.Java, "public class Root {\n private String name;\n public String getname() { return name; }\n public void setname(String name) { this.name = name; }\n}")]
public void ConvertTo_ValidJson_ReturnsExpectedResult(string json, SerializableLanguage language, string expected)
{
// Act
var result = JsonSerializer.ConvertTo(json, language);

// Assert
Assert.AreEqual(Normalize(expected), Normalize(result));
}

[TestMethod]
public void ConvertTo_InvalidJson_ThrowsException()
{
// Arrange
string invalidJson = "invalid json";

// Act
try
{
// Stupidly, JsonReaderException is internal, so it can't be caught.
// Instead, we need to compare the exception message with the expected message.
JsonSerializer.ConvertTo(invalidJson, SerializableLanguage.CSharp);
}
catch (Exception ex)
{
// Assert
Assert.AreEqual("'i' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.", ex.Message);
}
}

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ConvertTo_NullJson_ThrowsException()
{
// Arrange
string? invalidJson = null;

JsonSerializer.ConvertTo(invalidJson, SerializableLanguage.CSharp);
}

[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void ConvertTo_UnsupportedLanguage_ThrowsNotSupportedException()
{
// Arrange
string json = "{\"name\":\"John\"}";

// Act
JsonSerializer.ConvertTo(json, (SerializableLanguage)0xFFFFFF);
}

[TestMethod]
public void ConvertTo_JsonArray_WrapsInRootObject()
{
// Arrange
string jsonArray = "[{\"name\":\"John\"}]";
string expected = """
public class Root
{
public List<Items> Items { get; set; }
}
public class ItemsItem
{
public string name { get; set; }
}
public class Items
{
public List<ItemsItem> Items { get; set; } = new List<ItemsItem>();
}
""";
// Act
var result = JsonSerializer.ConvertTo(jsonArray, SerializableLanguage.CSharp);

// Assert
Assert.AreEqual(Normalize(expected), Normalize(result));
}

private static string Normalize(string input)
=> input.Replace("\r\n", "\n").Trim();
}
97 changes: 97 additions & 0 deletions tests/Riverside.JsonBinder.Tests/LanguageSerializerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text.Json.Nodes;
using Riverside.JsonBinder.Serialization;

namespace Riverside.JsonBinder.Tests;

[TestClass]
public class LanguageSerializerTests
{
private static readonly Dictionary<SerializableLanguage, Type> SerializerTypes = new()
{
{ SerializableLanguage.CSharp, typeof(CSharpSerializer) },
{ SerializableLanguage.Python, typeof(PythonSerializer) },
{ SerializableLanguage.Java, typeof(JavaSerializer) },
{ SerializableLanguage.JavaScript, typeof(JavaScriptSerializer) },
{ SerializableLanguage.TypeScript, typeof(TypeScriptSerializer) },
{ SerializableLanguage.PHP, typeof(PHPSerializer) },
{ SerializableLanguage.Ruby, typeof(RubySerializer) },
{ SerializableLanguage.Swift, typeof(SwiftSerializer) }
};

[TestMethod]
[DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)]
public void GenerateClasses_ValidJson_ReturnsExpectedResult(SerializableLanguage language, string json, string expected)
{
// Arrange
if (!SerializerTypes.TryGetValue(language, out var serializerType) || serializerType == null)
{
Assert.Fail($"Serializer for language {language} not found.");
return;
}

var serializer = (LanguageSerializer?)Activator.CreateInstance(serializerType);
Assert.IsNotNull(serializer, $"Failed to create instance of {serializerType}");

// Act
var jsonNode = JsonNode.Parse(json);
Assert.IsNotNull(jsonNode, "Failed to parse JSON");

var result = serializer.GenerateClasses(jsonNode, "Root");

// Assert
Assert.AreEqual(expected.Normalize(), result.Normalize());
}

[TestMethod]
[DynamicData(nameof(GetInvalidJsonTestData), DynamicDataSourceType.Method)]
public void GenerateClasses_InvalidJson_ThrowsException(SerializableLanguage language, string invalidJson)
{
// Arrange
if (!SerializerTypes.TryGetValue(language, out var serializerType) || serializerType == null)
{
Assert.Fail($"Serializer for language {language} not found.");
return;
}

var serializer = (LanguageSerializer?)Activator.CreateInstance(serializerType);
Assert.IsNotNull(serializer, $"Failed to create instance of {serializerType}");

// Act
try
{
var jsonNode = JsonNode.Parse(invalidJson);
Assert.IsNotNull(jsonNode, "Failed to parse JSON");

serializer.GenerateClasses(jsonNode, "Root");
}
catch (Exception ex)
{
// Assert
Assert.AreEqual("'i' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.", ex.Message);
}
}

public static IEnumerable<object[]> GetTestData()
{
yield return new object[] { SerializableLanguage.CSharp, "{\"name\":\"John\"}", "public class Root\n{\n public string name { get; set; }\n}" };
yield return new object[] { SerializableLanguage.Python, "{\"name\":\"John\"}", "class Root:\n def __init__(self):\n self.name: str = None" };
yield return new object[] { SerializableLanguage.Java, "{\"name\":\"John\"}", "public class Root {\n private String name;\n public String getname() { return name; }\n public void setname(String name) { this.name = name; }\n}" };
yield return new object[] { SerializableLanguage.JavaScript, "{\"name\":\"John\"}", "class Root {\n constructor() {\n this.name = null;\n }\n}" };
yield return new object[] { SerializableLanguage.TypeScript, "{\"name\":\"John\"}", "class Root {\n constructor() {\n this.name = null;\n }\n}" };
yield return new object[] { SerializableLanguage.PHP, "{\"name\":\"John\"}", "class Root {\n public $name;\n}" };
yield return new object[] { SerializableLanguage.Ruby, "{\"name\":\"John\"}", "class Root\n attr_accessor :name\nend" };
yield return new object[] { SerializableLanguage.Swift, "{\"name\":\"John\"}", "struct Root {\n var name: String?\n}" };
}

public static IEnumerable<object[]> GetInvalidJsonTestData()
{
yield return new object[] { SerializableLanguage.CSharp, "invalid json" };
yield return new object[] { SerializableLanguage.Python, "invalid json" };
yield return new object[] { SerializableLanguage.Java, "invalid json" };
yield return new object[] { SerializableLanguage.JavaScript, "invalid json" };
yield return new object[] { SerializableLanguage.TypeScript, "invalid json" };
yield return new object[] { SerializableLanguage.PHP, "invalid json" };
yield return new object[] { SerializableLanguage.Ruby, "invalid json" };
yield return new object[] { SerializableLanguage.Swift, "invalid json" };
}
}
1 change: 1 addition & 0 deletions tests/Riverside.JsonBinder.Tests/MSTestSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
24 changes: 24 additions & 0 deletions tests/Riverside.JsonBinder.Tests/Riverside.JsonBinder.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="MSTest.Sdk/3.6.4">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!--
Displays error on console in addition to the log file. Note that this feature comes with a performance impact.
For more information, visit https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-integration-dotnet-test#show-failure-per-test
-->
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
</PropertyGroup>

<PropertyGroup>
<NoWarn>1701;1702;CA1707;CS1591</NoWarn>
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Riverside.JsonBinder\Riverside.JsonBinder.csproj" />
</ItemGroup>

</Project>

0 comments on commit 05585c9

Please sign in to comment.