Files
step3/api/QuestionsApi.Tests/ChoicePoolTests.cs
T

177 lines
6.6 KiB
C#

using QuestionsApi.Models;
using QuestionsApi.Services;
using Xunit;
namespace QuestionsApi.Tests;
public sealed class ChoicePoolTests
{
[Fact]
public void LegacyResponseContainsFiveChoicesAndAlwaysIncludesCorrectAnswer()
{
var question = BuildQuestion(9);
for (var iteration = 0; iteration < 50; iteration++)
{
var response = QuestionResponse.FromModel(question, includeChoicePool: false);
Assert.Equal(5, response.Choices.Count);
Assert.Equal(5, response.Choices.Select(choice => choice.Id).Distinct().Count());
Assert.Contains(response.Choices, choice => choice.Id == question.CorrectChoiceId);
Assert.All(response.ChoiceExplanations.Keys, id => Assert.Contains(response.Choices, choice => choice.Id == id));
Assert.All(response.WrongAnswerFollowups.Keys, id => Assert.Contains(response.Choices, choice => choice.Id == id));
}
}
[Fact]
public void PoolCapableResponseContainsCompletePool()
{
var question = BuildQuestion(9);
var response = QuestionResponse.FromModel(question, includeChoicePool: true);
Assert.Equal(9, response.Choices.Count);
Assert.Single(response.References);
Assert.Equal("https://example.org/guideline", response.References[0].Url);
}
[Fact]
public void ExamReviewContainsOnlyChoicesPresentedDuringTheExam()
{
var question = BuildQuestion(9);
var presented = new[] { "F", "A", "C", "H", "D" };
var response = QuestionResponse.FromModelWithChoices(question, presented);
Assert.Equal(presented, response.Choices.Select(choice => choice.Id));
Assert.Equal(presented.Order(), response.ChoiceExplanations.Keys.Order());
Assert.DoesNotContain("B", response.Choices.Select(choice => choice.Id));
}
[Fact]
public void PublishedQuestionRequiresCompletePerChoiceTeachingContent()
{
var request = BuildRequest(6);
Assert.Null(QuestionValidator.Validate(request));
request.WrongAnswerFollowups.Remove("F");
Assert.Contains("every incorrect choice", QuestionValidator.Validate(request));
}
[Fact]
public void PublishedQuestionRequiresAtLeastFiveUniqueChoices()
{
var request = BuildRequest(4);
Assert.Contains("at least five", QuestionValidator.Validate(request));
request = BuildRequest(5);
request.Choices[4].Id = "A";
Assert.Contains("unique", QuestionValidator.Validate(request));
}
[Fact]
public void PublishedQuestionRequiresVerificationReferencesButDraftDoesNot()
{
var request = BuildRequest(5);
request.References.Clear();
Assert.Contains("verification reference", QuestionValidator.Validate(request));
request.IsPublished = false;
Assert.Null(QuestionValidator.Validate(request));
}
[Theory]
[InlineData("http://example.org/guideline", "HTTPS")]
[InlineData("https://example.org/guideline", null)]
public void VerificationReferencesValidateUrls(string url, string? expectedError)
{
var request = BuildRequest(5);
request.References[0].Url = url;
var validation = QuestionValidator.Validate(request);
if (expectedError is null)
Assert.Null(validation);
else
Assert.Contains(expectedError, validation);
}
[Fact]
public void VerificationReferencesRequireIsoAccessDateAndUniqueUrls()
{
var request = BuildRequest(5);
request.References[0].AccessedOn = "July 13, 2026";
Assert.Contains("YYYY-MM-DD", QuestionValidator.Validate(request));
request = BuildRequest(5);
request.References.Add(new QuestionReference
{
Title = "Second source",
Publisher = "Publisher",
Url = request.References[0].Url.ToUpperInvariant(),
Supports = "Another claim",
AccessedOn = "2026-07-13",
Reviewer = "Reviewer"
});
Assert.Contains("unique", QuestionValidator.Validate(request));
}
private static Question BuildQuestion(int count)
{
var request = BuildRequest(count);
return new Question
{
ExternalId = request.ExternalId,
Stem = request.Stem,
LeadIn = request.LeadIn,
Choices = request.Choices,
CorrectChoiceId = request.CorrectChoiceId,
Explanation = request.Explanation,
ChoiceExplanations = request.ChoiceExplanations,
WrongAnswerFollowups = request.WrongAnswerFollowups,
ChoiceSelectionPercentages = request.ChoiceSelectionPercentages,
References = request.References
};
}
private static UpsertQuestionRequest BuildRequest(int count)
{
var ids = Enumerable.Range(0, count).Select(index => ((char)('A' + index)).ToString()).ToList();
var choices = ids.Select(id => new QuestionChoice { Id = id, Text = $"Choice {id}" }).ToList();
return new UpsertQuestionRequest
{
ExternalId = "choice-pool-test",
Stem = "A complete clinical vignette.",
LeadIn = "Which option is most appropriate?",
Choices = choices,
CorrectChoiceId = "A",
Explanation = "Explanation",
ChoiceExplanations = ids.ToDictionary(id => id, id => id == "A" ? "Correct." : "Incorrect."),
WrongAnswerFollowups = ids.Where(id => id != "A").ToDictionary(
id => id,
id => new WrongAnswerFollowup
{
Prompt = "Which finding makes this less appropriate?",
Choices =
[
new QuestionChoice { Id = "A", Text = "Finding one" },
new QuestionChoice { Id = "B", Text = "Finding two" }
],
CorrectChoiceId = "A",
TeachingPoint = "Teaching point"
}),
ChoiceSelectionPercentages = ids.ToDictionary(id => id, _ => 0),
Discipline = "Medicine",
System = "Cardiovascular System",
Difficulty = 2,
References =
[
new QuestionReference
{
Title = "Clinical guideline",
Publisher = "Authoritative publisher",
Url = "https://example.org/guideline",
Supports = "The keyed answer and explanation.",
AccessedOn = "2026-07-13",
Reviewer = "Reviewer"
}
],
IsPublished = true
};
}
}