From fc929329988eee9642fdcf20fc4d8073022f518a Mon Sep 17 00:00:00 2001 From: Manas <119405883+manas-yu@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:58:14 +0530 Subject: [PATCH] Fix #4097: Adding tests for math utils (#5627) ## Explanation Fix #4097 This PR adds test files for the `FractionSubject`, `RealSubject`, `MathExpressionSubject`, and `MathEquationSubject` classes and includes a `BUILD.bazel` file to enable building and running these tests. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --- .../oppia/android/testing/math/BUILD.bazel | 69 +++ .../testing/math/FractionSubjectTest.kt | 206 +++++++++ .../testing/math/MathEquationSubjectTest.kt | 394 ++++++++++++++++++ .../testing/math/MathExpressionSubjectTest.kt | 371 +++++++++++++++++ .../android/testing/math/RealSubjectTest.kt | 204 +++++++++ 5 files changed, 1244 insertions(+) create mode 100644 testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel create mode 100644 testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt diff --git a/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel new file mode 100644 index 00000000000..354acf48077 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel @@ -0,0 +1,69 @@ +""" +Tests for math-related test utilities. +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "FractionSubjectTest", + srcs = ["FractionSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.FractionSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "MathEquationSubjectTest", + srcs = ["MathEquationSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.MathEquationSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "MathExpressionSubjectTest", + srcs = ["MathExpressionSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.MathExpressionSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "RealSubjectTest", + srcs = ["RealSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.RealSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) diff --git a/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt new file mode 100644 index 00000000000..79b2873f98e --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt @@ -0,0 +1,206 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.Fraction + +/** Tests for [FractionSubject]. */ +@RunWith(JUnit4::class) +class FractionSubjectTest { + + @Test + fun testHasNegativeProperty_withNegativeFraction_matchesTrue() { + val fraction = Fraction.newBuilder() + .setIsNegative(true) + .build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isTrue() + } + + @Test + fun testHasNegativeProperty_withPositiveFraction_matchesFalse() { + val fraction = Fraction.newBuilder() + .setIsNegative(false) + .build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isFalse() + } + + @Test + fun testHasNegativeProperty_defaultValue_matchesFalse() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isFalse() + } + + @Test + fun testHasWholeNumber_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(5) + .build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(5) + } + + @Test + fun testHasWholeNumber_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasWholeNumber_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testHasNumerator_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setNumerator(3) + .build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(3) + } + + @Test + fun testHasNumerator_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setNumerator(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasNumerator_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(0) + } + + @Test + fun testHasDenominator_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(4) + } + + @Test + fun testHasDenominator_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setDenominator(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasDenominator_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(0) + } + + @Test + fun testEvaluatesToDouble_withProperFraction_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setNumerator(3) + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(0.75) + } + + @Test + fun testEvaluatesToDouble_withImproperFraction_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setNumerator(5) + .setDenominator(2) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(2.5) + } + + @Test + fun testEvaluatesToDouble_withMixedNumber_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(2) + .setNumerator(3) + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(2.75) + } + + @Test + fun testEvaluatesToDouble_withNegativeValue_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(2) + .setNumerator(1) + .setDenominator(2) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(-2.5) + } + + @Test + fun testProtoEquality_withIdenticalValues_areEqual() { + val fraction1 = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + val fraction2 = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + assertThat(fraction1).isEqualTo(fraction2) + } + + @Test + fun testProtoSerialization_withComplexFraction_maintainsValues() { + val originalFraction = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + val bytes = originalFraction.toByteArray() + val deserializedFraction = Fraction.parseFrom(bytes) + + FractionSubject.assertThat(deserializedFraction).apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(2) + hasDenominatorThat().isEqualTo(5) + } + } + + @Test + fun testDefaultInstance_hasDefaultValues() { + val defaultFraction = Fraction.getDefaultInstance() + + FractionSubject.assertThat(defaultFraction).apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(0) + hasDenominatorThat().isEqualTo(0) + } + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt new file mode 100644 index 00000000000..7a9a6b8b62f --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt @@ -0,0 +1,394 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real + +/** Tests for [MathEquationSubject]. */ +@RunWith(JUnit4::class) +class MathEquationSubjectTest { + + @Test + fun testHasLeftHandSide_withValidExpression_matchesExpression() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + + @Test + fun testHasLeftHandSide_withDefaultExpression_hasNoExpressionType() { + val equation = MathEquation.getDefaultInstance() + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().isEqualTo( + MathExpression.getDefaultInstance() + ) + } + + @Test + fun testHasRightHandSide_withValidExpression_matchesExpression() { + val equation = createEquation( + leftSide = createConstantExpression(0), + rightSide = createConstantExpression(10) + ) + + MathEquationSubject.assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + } + + @Test + fun testHasRightHandSide_withDefaultExpression_hasNoExpressionType() { + val equation = MathEquation.getDefaultInstance() + + MathEquationSubject.assertThat(equation).hasRightHandSideThat().isEqualTo( + MathExpression.getDefaultInstance() + ) + } + + @Test + fun testConvertsToLatex_simpleEquation_producesCorrectString() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(10) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("5 = 10") + } + + @Test + fun testConvertsToLatex_withDivision_retainsDivisionOperator() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(10), + createConstantExpression(2) + ), + rightSide = createConstantExpression(5) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("10 \\div 2 = 5") + } + + @Test + fun testConvertsToLatexWithFractions_withDivision_producesFractionNotation() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(10), + createConstantExpression(2) + ), + rightSide = createConstantExpression(5) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{10}{2} = 5") + } + + @Test + fun testConvertsToLatex_complexExpression_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("3 + 4 \\times x = 0") + } + + @Test + fun testLeftHandSide_wrongExpression_failsWithAppropriateMessage() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(0) + ) + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 6") + } + + @Test + fun testRightHandSide_wrongExpression_failsWithAppropriateMessage() { + val equation = createEquation( + leftSide = createConstantExpression(0), + rightSide = createConstantExpression(10) + ) + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(11) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 11") + } + + @Test + fun testConvertsToLatex_withNestedOperations_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(2) + ), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(3), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("-2 + 3 \\times x = 0") + } + + @Test + fun testConvertsToLatexWithFractions_nestedFractions_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(1), + createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(2), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{1}{\\frac{2}{x}} = 0") + } + + @Test + fun testConvertsToLatex_withUnaryOperationInFraction_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(1) + ), + createConstantExpression(2) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{-1}{2} = 0") + } + + @Test + fun testConvertsToLatex_withFunctionCallInComplexExpression_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(1), + createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(4), + createVariableExpression("x") + ) + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("1 + \\sqrt{4 + x} = 0") + } + + @Test + fun testConvertsToLatex_withInvalidExpression_fails() { + val equation = MathEquation.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("5 = 0") + } + assertThat(exception).hasMessageThat().contains( + "expected: 5 = 0\n" + + "but was : =" + ) + } + + @Test + fun testConvertsWithFractionsToLatex_withInvalidExpression_fails() { + val equation = MathEquation.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{1}{2} = 0") + } + assertThat(exception).hasMessageThat().contains( + "expected: \\frac{1}{2} = 0\n" + + "but was : =" + ) + } + + @Test + fun testHasLeftHandSide_withComplexNestedExpression_matchesExpression() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createVariableExpression("x") + ) + ), + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(3) + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + private fun createFunctionCall( + functionType: MathFunctionCall.FunctionType, + argument: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(functionType) + .setArgument(argument) + ) + .build() + } + + private fun createUnaryOperation( + operator: MathUnaryOperation.Operator, + operand: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(operator) + .setOperand(operand) + ) + .build() + } + + private fun createEquation( + leftSide: MathExpression, + rightSide: MathExpression + ): MathEquation { + return MathEquation.newBuilder() + .setLeftSide(leftSide) + .setRightSide(rightSide) + .build() + } + + private fun createConstantExpression(value: Int): MathExpression { + return MathExpression.newBuilder() + .setConstant(Real.newBuilder().setInteger(value)) + .build() + } + + private fun createVariableExpression(name: String): MathExpression { + return MathExpression.newBuilder() + .setVariable(name) + .build() + } + + private fun createBinaryOperation( + operator: MathBinaryOperation.Operator, + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(operator) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt new file mode 100644 index 00000000000..d5e29fc9759 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt @@ -0,0 +1,371 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real + +/** Tests for [MathExpressionSubject]. */ +@RunWith(JUnit4::class) +class MathExpressionSubjectTest { + + @Test + fun testConstantExpression_withInteger_matchesStructure() { + val expression = createConstantExpression(5) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + + @Test + fun testConstantExpression_withWrongValue_fails() { + val expression = createConstantExpression(5) + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 6") + } + + @Test + fun testVariableExpression_matchesStructure() { + val expression = createVariableExpression("x") + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testVariableExpression_withWrongName_fails() { + val expression = createVariableExpression("x") + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + } + assertThat(exception).hasMessageThat().contains("expected: y") + } + + @Test + fun testBinaryOperation_addition_matchesStructure() { + val expression = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createConstantExpression(4) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testBinaryOperation_multiplication_withImplicit_matchesStructure() { + val expression = createImplicitMultiplication( + createConstantExpression(2), + createConstantExpression(3) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testUnaryOperation_negation_matchesStructure() { + val expression = createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(5) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testFunctionCall_squareRoot_matchesStructure() { + val expression = createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createConstantExpression(16) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(16) + } + } + } + } + } + + @Test + fun testComplexExpression_matchesStructure() { + // Creates expression: 3 + 4 * (-5) + val expression = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(5) + ) + ) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + @Test + fun testGroupExpression_matchesStructure() { + val expression = createGroupExpression(createConstantExpression(42)) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(42) + } + } + } + } + + @Test + fun testExpression_withUnsetType_fails() { + val expression = MathExpression.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + assertThat(exception).hasMessageThat().contains("EXPRESSIONTYPE_NOT_SET") + } + + @Test + fun testBinaryOperation_withUnsetOperator_fails() { + val expression = MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setLeftOperand(createConstantExpression(3)) + .setRightOperand(createConstantExpression(4)) + ) + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + assertThat(exception).hasMessageThat().contains("Expected binary operation with operator") + } + + @Test + fun testVariableExpression_withNullName_fails() { + val expression = MathExpression.newBuilder() + .setVariable("") + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isNotEmpty() + } + } + } + assertThat(exception).hasMessageThat().contains("expected not to be empty") + } + + @Test + fun testFunctionCall_withMissingArgument_fails() { + val expression = MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(MathFunctionCall.FunctionType.SQUARE_ROOT) + ) + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(16) + } + } + } + } + } + assertThat(exception).hasMessageThat().contains("EXPRESSIONTYPE_NOT_SET") + } + + /** Creates a constant [MathExpression] with the specified integer value. */ + private fun createConstantExpression(value: Int): MathExpression { + return MathExpression.newBuilder() + .setConstant(Real.newBuilder().setInteger(value)) + .build() + } + + /** Creates a variable [MathExpression] with the specified variable name. */ + private fun createVariableExpression(name: String): MathExpression { + return MathExpression.newBuilder() + .setVariable(name) + .build() + } + + /** Creates a binary operation [MathExpression] with the specified operator and operands. */ + private fun createBinaryOperation( + operator: MathBinaryOperation.Operator, + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(operator) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } + + /** Creates an implicit multiplication [MathExpression] between two operands. */ + private fun createImplicitMultiplication( + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(MathBinaryOperation.Operator.MULTIPLY) + .setIsImplicit(true) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } + + /** Creates a unary operation [MathExpression] with the specified operator and operand. */ + private fun createUnaryOperation( + operator: MathUnaryOperation.Operator, + operand: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(operator) + .setOperand(operand) + ) + .build() + } + + /** Creates a function call [MathExpression] with the specified function type and argument. */ + private fun createFunctionCall( + functionType: MathFunctionCall.FunctionType, + argument: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(functionType) + .setArgument(argument) + ) + .build() + } + + /** Creates a group [MathExpression] that wraps the specified inner expression. */ + private fun createGroupExpression(inner: MathExpression): MathExpression { + return MathExpression.newBuilder() + .setGroup(inner) + .build() + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt new file mode 100644 index 00000000000..343321832f6 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt @@ -0,0 +1,204 @@ +package org.oppia.android.testing.math + +import android.annotation.SuppressLint +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.Real + +/** Tests for [RealSubject]. */ +@SuppressLint("CheckResult") +@RunWith(JUnit4::class) +class RealSubjectTest { + + @Test + fun testRational_withRationalValue_canAccessRationalSubject() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val subject = RealSubject.assertThat(real).isRationalThat() + + subject.hasNumeratorThat().isEqualTo(1) + subject.hasDenominatorThat().isEqualTo(2) + } + + @Test + fun testRational_withIrrationalValue_fails() { + val real = Real.newBuilder().setIrrational(3.14).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: IRRATIONAL" + ) + } + + @Test + fun testRational_withIntegerValue_fails() { + val real = Real.newBuilder().setInteger(42).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: INTEGER" + ) + } + + @Test + fun testIrrational_withIrrationalValue_canAccessDoubleSubject() { + val real = Real.newBuilder().setIrrational(3.14159).build() + + val subject = RealSubject.assertThat(real).isIrrationalThat() + + subject.isWithin(0.00001).of(3.14159) + } + + @Test + fun testIrrational_withRationalValue_fails() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: RATIONAL" + ) + } + + @Test + fun testIrrational_withIntegerValue_fails() { + val real = Real.newBuilder().setInteger(42).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: INTEGER" + ) + } + + @Test + fun testInteger_withIntegerValue_canAccessIntegerSubject() { + val real = Real.newBuilder().setInteger(42).build() + + val subject = RealSubject.assertThat(real).isIntegerThat() + + subject.isEqualTo(42) + } + + @Test + fun testInteger_withRationalValue_fails() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: RATIONAL" + ) + } + + @Test + fun testInteger_withIrrationalValue_fails() { + val real = Real.newBuilder().setIrrational(3.14).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: IRRATIONAL" + ) + } + + @Test + fun testNull_fails() { + val exception = assertThrows(IllegalStateException::class.java) { + RealSubject.assertThat(null).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains("Expected real to be non-null") + } + + @Test + fun testUnsetType_asRational_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testUnsetType_asIrrational_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testUnsetType_asInteger_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testInheritedProtoMethods_work() { + val real = Real.newBuilder().setInteger(42).build() + + RealSubject.assertThat(real).isNotNull() + RealSubject.assertThat(real).isNotEqualTo(Real.getDefaultInstance()) + } + + private fun assertThrows( + expectedType: Class, + runnable: () -> Unit + ): T { + try { + runnable() + } catch (t: Throwable) { + if (expectedType.isInstance(t)) { + @Suppress("UNCHECKED_CAST") + return t as T + } + throw AssertionError( + "Expected ${expectedType.simpleName} but got ${t.javaClass.simpleName}", + t + ) + } + throw AssertionError( + "Expected ${expectedType.simpleName} to be thrown but nothing was thrown" + ) + } +}