Skip to content

Commit b91c292

Browse files
committed
Merge branch 'maintenance/SqlTopicRepository-Load-tests' into develop
It's not practical to unit test the `SqlTopicRepository` currently without first investing into more sophisticated testing infrastructure in order to e.g. mock the all of the complex objects that are involved, such as `SqlConnection`, `SqlCommand`, `SqlDataReader`, &c. That said, much of the complex logic is part of the `Load()` method, and that is largely handled by a series of `SqlDataReader` extensions. It's still difficult to test a `SqlDataReader`, since it doesn't have a public constructor and can only be created by a `SqlCommand`. But the `SqlDataReader` implements the `IDataReader` interface, which is shared by the `DataTableReader` class. As such, we generalize (most of*) the extension methods to work against `IDataReader`, and use that to test the extension methods against `DataTable` instances which mimic the data we'd expect to get back from an actual SQL query. To support this, we've created a series of `DataTable` derivatives which map to each of the SQL tables we wish to create a stub for. These include e.g. `TopicsDataTable`, `AttributesDataTable`, `RelationshipsDataTable`, &c. Each one not only defines the schema for each column, but also includes a strongly typed `AddRow()` method to simplify adding records. Using this, we're able to successfully write unit tests to evaluate (most of*) the extension methods. This allows us to confirm not only that topics, attributes, relationships, and references can be created, but also more nuanced behavior such as `referenceTopic` (for identifying relationships within the existing topic graph) and the new `IsFullyLoaded` tracking (for determining if any relationships failed to be identified). **Note:** Most of the extension methods are actually private. As a result, we test them indirectly by passing the tables to the main `LoadTopicGraph()` extension method, which composites the calls to the private extensions. If empty data tables are passed to it, it skips calls for the associated record set, thus keeping the unit tests focused on the relevant methods. The `LoadTopicGraph()` extension method itself is still `internal`, but the internals of `SqlTopicRepository` are exposed to the `OnTopic.Tests` project. * The `SetExtendedAttributes()` extension method is not able to be correctly tested since it relies upon the `GetSqlXml()` method, which is specific to the `SqlDataReader`, and thus cannot be mimicked with a `DataTableReader`. All other extensions methods are evaluated.
2 parents c1ea208 + 9bc4c57 commit b91c292

9 files changed

+763
-27
lines changed

OnTopic.Data.Sql/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
| Project Topics Library
55
\=============================================================================================================================*/
66
using System;
7+
using System.Runtime.CompilerServices;
78
using System.Runtime.InteropServices;
89

910
/*==============================================================================================================================
@@ -13,4 +14,5 @@
1314
\-----------------------------------------------------------------------------------------------------------------------------*/
1415
[assembly: ComVisible(false)]
1516
[assembly: CLSCompliant(true)]
17+
[assembly: InternalsVisibleTo("OnTopic.Tests")]
1618
[assembly: Guid("1de1f923-c7c2-435b-b49a-975acbcb5ff0")]

OnTopic.Data.Sql/SqlDataReaderExtensions.cs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
\=============================================================================================================================*/
66
using System;
77
using System.Collections.Generic;
8+
using System.Data;
89
using System.Diagnostics;
910
using System.Linq;
1011
using System.Net;
@@ -19,7 +20,7 @@ namespace OnTopic.Data.Sql {
1920
| CLASS: SQL DATA READER EXTENSIONS
2021
\---------------------------------------------------------------------------------------------------------------------------*/
2122
/// <summary>
22-
/// Extension methods for the <see cref="SqlDataReader"/> class.
23+
/// Extension methods for the <see cref="IDataReader"/> class.
2324
/// </summary>
2425
/// <remarks>
2526
/// Most of the extensions are optimized for reading the data returned from the <c>GetTopics</c> and <c>GetTopicVersion</c>
@@ -36,10 +37,10 @@ internal static class SqlDataReaderExtensions {
3637
| METHOD: LOAD TOPIC GRAPH
3738
\-------------------------------------------------------------------------------------------------------------------------*/
3839
/// <summary>
39-
/// Given a <see cref="SqlDataReader"/> from a call to the <c>GetTopics</c> stored procedure, will extract a list of
40+
/// Given a <see cref="IDataReader"/> from a call to the <c>GetTopics</c> stored procedure, will extract a list of
4041
/// topics and populate their attributes, relationships, and children.
4142
/// </summary>
42-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
43+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
4344
/// <param name="referenceTopic">
4445
/// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references
4546
/// and relationships, including <see cref="Topic.Parent"/>, are integrated with existing entities.
@@ -56,7 +57,7 @@ internal static class SqlDataReaderExtensions {
5657
/// thus external references aren't likely to be available.
5758
/// </param>
5859
internal static Topic LoadTopicGraph(
59-
this SqlDataReader reader,
60+
this IDataReader reader,
6061
Topic? referenceTopic = null,
6162
bool? markDirty = null,
6263
bool includeExternalReferences = true
@@ -65,6 +66,7 @@ internal static Topic LoadTopicGraph(
6566
/*----------------------------------------------------------------------------------------------------------------------
6667
| Establish topic index
6768
\---------------------------------------------------------------------------------------------------------------------*/
69+
var sqlDataReader = reader as SqlDataReader;
6870
var topics = referenceTopic is not null? referenceTopic.GetRootTopic().GetTopicIndex() : new();
6971
var rootTopicId = -1;
7072

@@ -101,7 +103,9 @@ internal static Topic LoadTopicGraph(
101103

102104
// Loop through each extended attribute record associated with a specific topic
103105
while (reader.Read()) {
104-
reader.SetExtendedAttributes(topics, markDirty);
106+
if (sqlDataReader is not null) {
107+
sqlDataReader.SetExtendedAttributes(topics, markDirty);
108+
}
105109
}
106110

107111
/*----------------------------------------------------------------------------------------------------------------------
@@ -162,15 +166,15 @@ internal static Topic LoadTopicGraph(
162166
/// Given the primary topic attributes from the <c>TopicIndex</c> view, establishes a barebones <see cref="Topic"/>
163167
/// instance and adds it to the <paramref name="topics"/> collection.
164168
/// </summary>
165-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
169+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
166170
/// <param name="topics">A <see cref="Dictionary{Int32, Topic}"/> of topics to be loaded.</param>
167171
/// <param name="markDirty">
168172
/// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it
169173
/// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that
170174
/// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update
171175
/// from being persisted to the data store on <see cref="Repositories.ITopicRepository.Save(Topic, Boolean)"/>.
172176
/// </param>
173-
private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool? markDirty) {
177+
private static void AddTopic(this IDataReader reader, TopicIndex topics, bool? markDirty) {
174178

175179
/*------------------------------------------------------------------------------------------------------------------------
176180
| Identify attributes
@@ -217,15 +221,15 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool?
217221
/// Given an attribute record from the <c>AttributeIndex</c> view, finds the associated <see cref="Topic"/> in the
218222
/// <paramref name="topics"/> collection, and sets the corresponding <see cref="Topic.Attributes"/> value.
219223
/// </summary>
220-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
224+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
221225
/// <param name="topics">A <see cref="Dictionary{Int32, Topic}"/> of topics to be loaded.</param>
222226
/// <param name="markDirty">
223227
/// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it
224228
/// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that
225229
/// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update
226230
/// from being persisted to the data store on <see cref="Repositories.ITopicRepository.Save(Topic, Boolean)"/>.
227231
/// </param>
228-
private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex topics, bool? markDirty) {
232+
private static void SetIndexedAttributes(this IDataReader reader, TopicIndex topics, bool? markDirty) {
229233

230234
/*------------------------------------------------------------------------------------------------------------------------
231235
| Identify attributes
@@ -335,15 +339,15 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex
335339
/// Topics can be cross-referenced with each other via a many-to-many relationships. Once the topics are populated in
336340
/// memory, loop through the data to create these associations.
337341
/// </remarks>
338-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
342+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
339343
/// <param name="topics">A <see cref="Dictionary{Int32, Topic}"/> of topics to be loaded.</param>
340344
/// <param name="markDirty">
341345
/// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it
342346
/// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that
343347
/// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update
344348
/// from being persisted to the data store on <see cref="Repositories.ITopicRepository.Save(Topic, Boolean)"/>.
345349
/// </param>
346-
private static void SetRelationships(this SqlDataReader reader, TopicIndex topics, bool? isDirty = false) {
350+
private static void SetRelationships(this IDataReader reader, TopicIndex topics, bool? isDirty = false) {
347351

348352
/*------------------------------------------------------------------------------------------------------------------------
349353
| Identify attributes
@@ -392,15 +396,15 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic
392396
/// Topics can be cross-referenced with each other topics via a one-to-one relationships. Once the topics are populated in
393397
/// memory, loop through the data to create these associations.
394398
/// </remarks>
395-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
399+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
396400
/// <param name="topics">A <see cref="Dictionary{Int32, Topic}"/> of topics to be loaded.</param>
397401
/// <param name="markDirty">
398402
/// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it
399403
/// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that
400404
/// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update
401405
/// from being persisted to the data store on <see cref="Repositories.ITopicRepository.Save(Topic, Boolean)"/>.
402406
/// </param>
403-
private static void SetReferences(this SqlDataReader reader, TopicIndex topics, bool? markDirty) {
407+
private static void SetReferences(this IDataReader reader, TopicIndex topics, bool? markDirty) {
404408

405409
/*------------------------------------------------------------------------------------------------------------------------
406410
| Identify attributes
@@ -444,9 +448,9 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics,
444448
/// version history is aggregated per topic to allow topic information to be rolled back to a specific date.While version
445449
/// content is not exposed directly via the Load() method, the metadata is.
446450
/// </remarks>
447-
/// <param name="reader">The <see cref="SqlDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
451+
/// <param name="reader">The <see cref="IDataReader"/> with output from the <c>GetTopics</c> stored procedure.</param>
448452
/// <param name="topics">A <see cref="Dictionary{Int32, Topic}"/> of topics to be loaded.</param>
449-
private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topics) {
453+
private static void SetVersionHistory(this IDataReader reader, TopicIndex topics) {
450454

451455
/*------------------------------------------------------------------------------------------------------------------------
452456
| Identify attributes
@@ -474,9 +478,9 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi
474478
/// <summary>
475479
/// Retrieves a string value by column name.
476480
/// </summary>
477-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
481+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
478482
/// <param name="columnName">The name of the column to retrieve the value from.</param>
479-
private static string GetString(this SqlDataReader reader, string columnName) =>
483+
private static string GetString(this IDataReader reader, string columnName) =>
480484
reader.GetString(reader.GetOrdinal(columnName));
481485

482486
/*==========================================================================================================================
@@ -485,9 +489,9 @@ private static string GetString(this SqlDataReader reader, string columnName) =>
485489
/// <summary>
486490
/// Retrieves a boolean value by column name.
487491
/// </summary>
488-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
492+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
489493
/// <param name="columnName">The name of the column to retrieve the value from.</param>
490-
private static bool GetBoolean(this SqlDataReader reader, string columnName) =>
494+
private static bool GetBoolean(this IDataReader reader, string columnName) =>
491495
reader.GetBoolean(reader.GetOrdinal(columnName));
492496

493497
/*==========================================================================================================================
@@ -496,9 +500,9 @@ private static bool GetBoolean(this SqlDataReader reader, string columnName) =>
496500
/// <summary>
497501
/// Retrieves an integer value by column name.
498502
/// </summary>
499-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
503+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
500504
/// <param name="columnName">The name of the column to retrieve the value from.</param>
501-
private static int GetInteger(this SqlDataReader reader, string columnName) =>
505+
private static int GetInteger(this IDataReader reader, string columnName) =>
502506
Int32.TryParse(reader.GetValue(reader.GetOrdinal(columnName)).ToString(), out var output)? output : -1;
503507

504508
/*==========================================================================================================================
@@ -507,9 +511,9 @@ private static int GetInteger(this SqlDataReader reader, string columnName) =>
507511
/// <summary>
508512
/// Retrieves a <see cref="Topic.Id"/> value by column name.
509513
/// </summary>
510-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
514+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
511515
/// <param name="columnName">The name of the column to retrieve the value from.</param>
512-
private static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") =>
516+
private static int GetTopicId(this IDataReader reader, string columnName = "TopicID") =>
513517
reader.GetInt32(reader.GetOrdinal(columnName));
514518

515519
/*==========================================================================================================================
@@ -518,9 +522,9 @@ private static int GetTopicId(this SqlDataReader reader, string columnName = "To
518522
/// <summary>
519523
/// Retrieves a <see cref="Topic.Id"/> value by column name, while accepting null values.
520524
/// </summary>
521-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
525+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
522526
/// <param name="columnName">The name of the column to retrieve the value from.</param>
523-
private static int? GetNullableTopicId(this SqlDataReader reader, string columnName = "TopicID") =>
527+
private static int? GetNullableTopicId(this IDataReader reader, string columnName = "TopicID") =>
524528
reader.IsDBNull(reader.GetOrdinal(columnName))? null : reader.GetInt32(reader.GetOrdinal(columnName));
525529

526530
/*==========================================================================================================================
@@ -529,8 +533,8 @@ private static int GetTopicId(this SqlDataReader reader, string columnName = "To
529533
/// <summary>
530534
/// Retrieves the version column, with precisions appropriate for setting the <see cref="Topic.VersionHistory"/>.
531535
/// </summary>
532-
/// <param name="reader">The <see cref="SqlDataReader"/> object.</param>
533-
private static DateTime GetVersion(this SqlDataReader reader) =>
536+
/// <param name="reader">The <see cref="IDataReader"/> object.</param>
537+
private static DateTime GetVersion(this IDataReader reader) =>
534538
reader.GetDateTime(reader.GetOrdinal("Version"));
535539

536540
} //Class

OnTopic.Tests/OnTopic.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
<ItemGroup>
4242
<ProjectReference Include="..\OnTopic.Data.Caching\OnTopic.Data.Caching.csproj" />
43+
<ProjectReference Include="..\OnTopic.Data.Sql\OnTopic.Data.Sql.csproj" />
4344
<ProjectReference Include="..\OnTopic.TestDoubles\OnTopic.TestDoubles.csproj" />
4445
<ProjectReference Include="..\OnTopic.ViewModels\OnTopic.ViewModels.csproj" />
4546
<ProjectReference Include="..\OnTopic\OnTopic.csproj" />

0 commit comments

Comments
 (0)