diff --git a/common/lib/calc/.coveragerc b/common/lib/calc/.coveragerc new file mode 100644 index 0000000000..352ddf399e --- /dev/null +++ b/common/lib/calc/.coveragerc @@ -0,0 +1,15 @@ +# .coveragerc for common/lib/calc +[run] +data_file = reports/common/lib/calc/.coverage +source = common/lib/calc +branch = true + +[report] +ignore_errors = True + +[html] +title = Calc Python Test Coverage Report +directory = reports/common/lib/calc/cover + +[xml] +output = reports/common/lib/calc/coverage.xml diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index bb1fb97153..2f33b66bfd 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -144,6 +144,8 @@ def evaluator(variables, functions, string, cs=False): return x def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 + # convert from pyparsing.ParseResults, which doesn't support '0 in x' + x = list(x) if len(x) == 1: return x[0] if 0 in x: @@ -230,27 +232,3 @@ def evaluator(variables, functions, string, cs=False): expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 expr = expr.setParseAction(sum_parse_action) return (expr + stringEnd).parseString(string)[0] - -if __name__ == '__main__': - variables = {'R1': 2.0, 'R3': 4.0} - functions = {'sin': numpy.sin, 'cos': numpy.cos} - print "X", evaluator(variables, functions, "10000||sin(7+5)-6k") - print "X", evaluator(variables, functions, "13") - print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13") - - print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2") - - print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5") - print evaluator({}, {}, "-1") - print evaluator({}, {}, "-(7+5)") - print evaluator({}, {}, "-0.33") - print evaluator({}, {}, "-.33") - print evaluator({}, {}, "5+1*j") - print evaluator({}, {}, "j||1") - print evaluator({}, {}, "e^(j*pi)") - print evaluator({}, {}, "fact(5)") - print evaluator({}, {}, "factorial(5)") - try: - print evaluator({}, {}, "5+7 QWSEKO") - except UndefinedVariable: - print "Successfully caught undefined variable" diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py new file mode 100644 index 0000000000..58d0860af6 --- /dev/null +++ b/common/lib/calc/tests/test_calc.py @@ -0,0 +1,356 @@ +""" +Unit tests for calc.py +""" + +import unittest +import numpy +import calc + + +class EvaluatorTest(unittest.TestCase): + """ + Run tests for calc.evaluator + Go through all functionalities as specifically as possible-- + work from number input to functions and complex expressions + Also test custom variable substitutions (i.e. + `evaluator({'x':3.0},{}, '3*x')` + gives 9.0) and more. + """ + + def test_number_input(self): + """ + Test different kinds of float inputs + """ + easy_eval = lambda x: calc.evaluator({}, {}, x) + + self.assertEqual(easy_eval("13"), 13) + self.assertEqual(easy_eval("3.14"), 3.14) + self.assertEqual(easy_eval(".618033989"), 0.618033989) + + self.assertEqual(easy_eval("-13"), -13) + self.assertEqual(easy_eval("-3.14"), -3.14) + self.assertEqual(easy_eval("-.618033989"), -0.618033989) + # See also test_exponential_answer and test_si_suffix + + def test_exponential_answer(self): + """ + Test for correct interpretation of scientific notation + """ + answer = 50 + correct_responses = ["50", "50.0", "5e1", "5e+1", + "50e0", "50.0e0", "500e-1"] + incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] + + for input_str in correct_responses: + result = calc.evaluator({}, {}, input_str) + fail_msg = "Expected '{0}' to equal {1}".format( + input_str, answer) + self.assertEqual(answer, result, msg=fail_msg) + + for input_str in incorrect_responses: + result = calc.evaluator({}, {}, input_str) + fail_msg = "Expected '{0}' to not equal {1}".format( + input_str, answer) + self.assertNotEqual(answer, result, msg=fail_msg) + + def test_si_suffix(self): + """ + Test calc.py's unique functionality of interpreting si 'suffixes'. + + For instance 'k' stand for 'kilo-' so '1k' should be 1,000 + """ + test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000), + ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074), + ('5.4m', 0.0054), ('8.7u', 0.0000087), + ('5.6n', 5.6e-9), ('4.2p', 4.2e-12)] + + for (expr, answer) in test_mapping: + tolerance = answer * 1e-6 # Make rel. tolerance, because of floats + fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}" + fail_msg = fail_msg.format(expr[-1], expr, answer) + self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer, + delta=tolerance, msg=fail_msg) + + def test_operator_sanity(self): + """ + Test for simple things like '5+2' and '5/2' + """ + var1 = 5.0 + var2 = 2.0 + operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)] + + for (operator, answer) in operators: + input_str = "{0} {1} {2}".format(var1, operator, var2) + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format( + operator, input_str, answer) + self.assertEqual(answer, result, msg=fail_msg) + + def test_raises_zero_division_err(self): + """ + Ensure division by zero gives an error + """ + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '1/0') + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '1/0.0') + self.assertRaises(ZeroDivisionError, calc.evaluator, + {'x': 0.0}, {}, '1/x') + + def test_parallel_resistors(self): + """ + Test the parallel resistor operator || + + The formula is given by + a || b || c ... + = 1 / (1/a + 1/b + 1/c + ...) + It is the resistance of a parallel circuit of resistors with resistance + a, b, c, etc&. See if this evaulates correctly. + """ + self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5) + self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) + self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) + + def test_parallel_resistors_with_zero(self): + """ + Check the behavior of the || operator with 0 + """ + self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1'))) + self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1'))) + self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1'))) + + def assert_function_values(self, fname, ins, outs, tolerance=1e-3): + """ + Helper function to test many values at once + + Test the accuracy of evaluator's use of the function given by fname + Specifically, the equality of `fname(ins[i])` against outs[i]. + This is used later to test a whole bunch of f(x) = y at a time + """ + + for (arg, val) in zip(ins, outs): + input_str = "{0}({1})".format(fname, arg) + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed on function {0}: '{1}' was not {2}".format( + fname, input_str, val) + self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) + + def test_trig_functions(self): + """ + Test the trig functions provided in calc.py + + which are: sin, cos, tan, arccos, arcsin, arctan + """ + + angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] + sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] + cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j] + tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j] + # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... + + self.assert_function_values('sin', angles, sin_values) + self.assert_function_values('cos', angles, cos_values) + self.assert_function_values('tan', angles, tan_values) + + # Include those where the real part is between -pi/2 and pi/2 + arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j'] + arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j] + self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles) + # Rather than throwing an exception, numpy.arcsin gives nan + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)'))) + # Disabled for now because they are giving a runtime warning... :-/ + + # Include those where the real part is between 0 and pi + arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j'] + arccos_angles = [0, 0.524, 0.628, 1 + 1j] + self.assert_function_values('arccos', arccos_inputs, arccos_angles) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)'))) + + # Has the same range as arcsin + arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j'] + arctan_angles = arcsin_angles + self.assert_function_values('arctan', arctan_inputs, arctan_angles) + + def test_other_functions(self): + """ + Test the non-trig functions provided in calc.py + + Specifically: + sqrt, log10, log2, ln, abs, + fact, factorial + """ + + # Test sqrt + self.assert_function_values('sqrt', + [0, 1, 2, 1024], # -1 + [0, 1, 1.414, 32]) # 1j + # sqrt(-1) is NAN not j (!!). + + # Test logs + self.assert_function_values('log10', + [0.1, 1, 3.162, 1000000, '1+j'], + [-1, 0, 0.5, 6, 0.151 + 0.341j]) + self.assert_function_values('log2', + [0.5, 1, 1.414, 1024, '1+j'], + [-1, 0, 0.5, 10, 0.5 + 1.133j]) + self.assert_function_values('ln', + [0.368, 1, 1.649, 2.718, 42, '1+j'], + [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]) + + # Test abs + self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) + + # Test factorial + fact_inputs = [0, 1, 3, 7] + fact_values = [1, 1, 6, 5040] + self.assert_function_values('fact', fact_inputs, fact_values) + self.assert_function_values('factorial', fact_inputs, fact_values) + + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)") + + def test_constants(self): + """ + Test the default constants provided in calc.py + + which are: j (complex number), e, pi, k, c, T, q + """ + + # Of the form ('expr', python value, tolerance (or None for exact)) + default_variables = [('j', 1j, None), + ('e', 2.7183, 1e-3), + ('pi', 3.1416, 1e-3), + # c = speed of light + ('c', 2.998e8, 1e5), + # 0 deg C = T Kelvin + ('T', 298.15, 0.01), + # Note k = scipy.constants.k = 1.3806488e-23 + ('k', 1.3806488e-23, 1e-26), + # Note q = scipy.constants.e = 1.602176565e-19 + ('q', 1.602176565e-19, 1e-22)] + for (variable, value, tolerance) in default_variables: + fail_msg = "Failed on constant '{0}', not within bounds".format( + variable) + result = calc.evaluator({}, {}, variable) + if tolerance is None: + self.assertEqual(value, result, msg=fail_msg) + else: + self.assertAlmostEqual(value, result, + delta=tolerance, msg=fail_msg) + + def test_complex_expression(self): + """ + Calculate combinations of operators and default functions + """ + + self.assertAlmostEqual( + calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), + 10.180, + delta=1e-3) + + self.assertAlmostEqual( + calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"), + 1.6, + delta=1e-3) + self.assertAlmostEqual( + calc.evaluator({}, {}, "10||sin(7+5)"), + -0.567, delta=0.01) + self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"), + 0.41, delta=0.01) + self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"), + 0.025, delta=1e-3) + self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"), + -1, delta=1e-5) + + def test_simple_vars(self): + """ + Substitution of variables into simple equations + """ + variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4} + + # Should not change value of constant + # even with different numbers of variables... + self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13) + self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13) + self.assertEqual(calc.evaluator(variables, {}, '13'), 13) + + # Easy evaluation + self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72) + self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91) + self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4) + + # Test a simple equation + self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'), + 21.25, delta=0.01) # = 3 * 9.72 - 7.91 + self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'), + 76.89, delta=0.01) + + self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13) + self.assertEqual(calc.evaluator(variables, {}, "13"), 13) + self.assertEqual( + calc.evaluator({ + 'a': 2.2997471478310274, 'k': 9, 'm': 8, + 'x': 0.66009498411213041}, + {}, "5"), + 5) + + def test_variable_case_sensitivity(self): + """ + Test the case sensitivity flag and corresponding behavior + """ + self.assertEqual( + calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), + 8.0) + + variables = {'t': 1.0} + self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0) + self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0) + self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0) + # Recall 'T' is a default constant, with value 298.15 + self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True), + 298, delta=0.2) + + def test_simple_funcs(self): + """ + Subsitution of custom functions + """ + variables = {'x': 4.712} + functions = {'id': lambda x: x} + self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) + self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) + self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712) + + functions.update({'f': numpy.sin}) + self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'), + -1, delta=1e-3) + + def test_function_case_sensitivity(self): + """ + Test the case sensitivity of functions + """ + functions = {'f': lambda x: x, + 'F': lambda x: x + 1} + # Test case insensitive evaluation + # Both evaulations should call the same function + self.assertEqual(calc.evaluator({}, functions, 'f(6)'), + calc.evaluator({}, functions, 'F(6)')) + # Test case sensitive evaluation + self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True), + calc.evaluator({}, functions, 'F(6)', cs=True)) + + def test_undefined_vars(self): + """ + Check to see if the evaluator catches undefined variables + """ + variables = {'R1': 2.0, 'R3': 4.0} + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {}, {}, "5+7 QWSEKO") + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {'r1': 5}, {}, "r1+r2") + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + variables, {}, "r1*r3", cs=True) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 8bf6954139..780c475b09 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -10,7 +10,6 @@ import random import unittest import textwrap import mock -import textwrap from . import new_loncapa_problem, test_system @@ -190,7 +189,7 @@ class SymbolicResponseTest(ResponseTest): def test_grade_single_input(self): problem = self.build_problem(math_display=True, - expect="2*x+3*y") + expect="2*x+3*y") # Correct answers correct_inputs = [ @@ -223,7 +222,6 @@ class SymbolicResponseTest(ResponseTest): for (input_str, input_mathml) in incorrect_inputs: self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect') - def test_complex_number_grade(self): problem = self.build_problem(math_display=True, expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]", @@ -241,7 +239,7 @@ class SymbolicResponseTest(ResponseTest): # Correct answer with mock.patch.object(requests, 'post') as mock_post: - # Simulate what the LaTeX-to-MathML server would + # Simulate what the LaTeX-to-MathML server would # send for the correct response input mock_post.return_value.text = correct_snuggletex_response @@ -323,7 +321,7 @@ class SymbolicResponseTest(ResponseTest): dynamath_input, expected_correctness): input_dict = {'1_2_1': str(student_input), - '1_2_1_dynamath': str(dynamath_input) } + '1_2_1_dynamath': str(dynamath_input)} correct_map = problem.grade_answers(input_dict) @@ -349,10 +347,18 @@ class OptionResponseTest(ResponseTest): class FormulaResponseTest(ResponseTest): + """ + Test the FormulaResponse class + """ from response_xml_factory import FormulaResponseXMLFactory xml_factory_class = FormulaResponseXMLFactory def test_grade(self): + """ + Test basic functionality of FormulaResponse + + Specifically, if it can understand equivalence of formulae + """ # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -373,6 +379,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_hint(self): + """ + Test the hint-giving functionality of FormulaResponse + """ # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -401,6 +410,10 @@ class FormulaResponseTest(ResponseTest): 'Try including the variable x') def test_script(self): + """ + Test if python script can be used to generate answers + """ + # Calculate the answer using a script script = "calculated_ans = 'x+x'" @@ -419,7 +432,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, '3*x', 'incorrect') def test_parallel_resistors(self): - """Test parallel resistors""" + """ + Test parallel resistors + """ sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)} # Test problem @@ -440,8 +455,11 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_default_variables(self): - """Test the default variables provided in common/lib/capa/capa/calc.py""" - # which are: j (complex number), e, pi, k, c, T, q + """ + Test the default variables provided in calc.py + + which are: j (complex number), e, pi, k, c, T, q + """ # Sample x in the range [-10,10] sample_dict = {'x': (-10, 10)} @@ -464,11 +482,14 @@ class FormulaResponseTest(ResponseTest): msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect)) def test_default_functions(self): - """Test the default functions provided in common/lib/capa/capa/calc.py""" - # which are: sin, cos, tan, sqrt, log10, log2, ln, - # arccos, arcsin, arctan, abs, - # fact, factorial + """ + Test the default functions provided in common/lib/capa/capa/calc.py + which are: + sin, cos, tan, sqrt, log10, log2, ln, + arccos, arcsin, arctan, abs, + fact, factorial + """ w = random.randint(3, 10) sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10] 'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs @@ -496,8 +517,10 @@ class FormulaResponseTest(ResponseTest): msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect)) def test_grade_infinity(self): - # This resolves a bug where a problem with relative tolerance would - # pass with any arbitrarily large student answer. + """ + Test that a large input on a problem with relative tolerance isn't + erroneously marked as correct. + """ sample_dict = {'x': (1, 2)} @@ -514,8 +537,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_grade_nan(self): - # Attempt to produce a value which causes the student's answer to be - # evaluated to nan. See if this is resolved correctly. + """ + Test that expressions that evaluate to NaN are not marked as correct. + """ sample_dict = {'x': (1, 2)} @@ -532,6 +556,18 @@ class FormulaResponseTest(ResponseTest): input_formula = "x + 0*1e999" self.assert_grade(problem, input_formula, "incorrect") + def test_raises_zero_division_err(self): + """ + See if division by zero raises an error. + """ + sample_dict = {'x': (1, 2)} + problem = self.build_problem(sample_dict=sample_dict, + num_samples=10, + tolerance="1%", + answer="x") # Answer doesn't matter + input_dict = {'1_2_1': '1/0'} + self.assertRaises(StudentInputError, problem.grade_answers, input_dict) + class StringResponseTest(ResponseTest): from response_xml_factory import StringResponseXMLFactory @@ -592,7 +628,7 @@ class StringResponseTest(ResponseTest): problem = self.build_problem( answer="Michigan", hintfn="gimme_a_hint", - script = textwrap.dedent(""" + script=textwrap.dedent(""" def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap): aid = answer_ids[0] answer = student_answers[aid] @@ -898,6 +934,14 @@ class NumericalResponseTest(ResponseTest): incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + def test_raises_zero_division_err(self): + """See if division by zero is handled correctly""" + problem = self.build_problem(question_text="What 5 * 10?", + explanation="The answer is 50", + answer="5e+1") # Answer doesn't matter + input_dict = {'1_2_1': '1/0'} + self.assertRaises(StudentInputError, problem.grade_answers, input_dict) + class CustomResponseTest(ResponseTest): from response_xml_factory import CustomResponseXMLFactory @@ -947,8 +991,8 @@ class CustomResponseTest(ResponseTest): # # 'answer_given' is the answer the student gave (if there is just one input) # or an ordered list of answers (if there are multiple inputs) - # - # The function should return a dict of the form + # + # The function should return a dict of the form # { 'ok': BOOL, 'msg': STRING } # script = textwrap.dedent(""" diff --git a/doc/testing.md b/doc/testing.md index e5d035d90e..4d286b1bcc 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -115,6 +115,11 @@ xmodule can be tested independently, with this: rake test_common/lib/xmodule +other module level tests include + +* `rake test_common/lib/capa` +* `rake test_common/lib/calc` + To run a single django test class: rake test_lms[courseware.tests.tests:testViewAuth]