How to create unitary tests
Kratos Multiphysics has a mechanism to automatically test your code called "KratosUnittest". If you are familiar with python, this module is basically an extension of Unittest[1] and you can expect to find every functionality that exists on in as well in KratosUnittest.
In order for your application to be robust, is recommended that you add unittests to it. Here we present some guidelines on how to do it.
Contents |
How to run tests
Kratos unittest are executed using the "run_tests.py" script located in the "kratos/kratos/python_scripts" folder.
Usage is the following:
python run_tests.py
you can specify the following options:
-l,--level: Select the suit. Values: "All", "Nighlty", "Small"(default) -v,--vervosity: Select the vervosity level of the output. Values: 0, 1 (default) , 2 -a,--applications: List of applications to run separated by ":". For example "-a KratosKore:IncompresibleFluidApplication" All applications compiled are run by default.
Basic structure
Tests are defined in a python script. To keep tests organized we recommend you to create the tests in your "application/tests/" directory.
Tests are organized in suites. Suites are collection of tests that will be run together as a package. In Kratos, we define three basic suites:
- All: which should contain all the tests
- Nighlty: which should contain a set of tests that could be executed in less than 10 min
- Small: which should contain a set of tests that could be executed in less than 1 min
All applications should implement this packages as they can be automatically run, for example in the "nighlty" runs.
In order to add tests you should create at least a couple of files, one to define the tests
and one to define the suits:
"application/tests/test_NAME_OF_OUR_APPLICATION.py"
for example:
kratos/applications/example_application/tests/test_example_application.py
General structure
This file will define the suites to run the tests. We define three different levels of suites in kratos:
- All: which should contain all the tests
- Nighlty: which should contain a set of tests that could be executed in less than 10 min
- Small: which should contain a set of tests that could be executed in less than 1 min
In order to add test to some of these suits, one can do it as shown in this example:
from __future__ import print_function, absolute_import, division # import Kratos from KratosMultiphysics import * # Import Kratos "wrapper" for unittests import KratosMultiphysics.KratosUnittest as KratosUnittest # Import the tests o test_classes to create the suites. For example from test_my_app_example_tests_1 import TestCase1 as TestCase1 from test_my_app_example_tests_2 import TestCase2 as TestCase2 def AssambleTestSuites(): Populates the test suites to run. Populates the test suites to run. At least, it should pupulate the suites: "small", "nighlty" and "all" Return ------ suites: A dictionary of suites The set of suites with its test_cases added. # Get the already defined suits suites = KratosUnittest.KratosSuites # Get the small suit and populate it with some tests from TestCase1 and TestCase2 smallSuite = suites['small'] smallSuite.addTest(TestCase1('test_example_small_boo_1')) smallSuite.addTest(TestCase2('test_example_small_foo_1')) # Get the small suit and populate it with some tests from TestCase1 and TestCase2 nightSuite = suites['nightly'] smallSuite.addTest(TestCase1('test_example_nightly_boo_1')) smallSuite.addTest(TestCase1('test_example_nightly_boo_2')) smallSuite.addTest(TestCase2('test_example_nightly_foo_1')) # Get the small suit and populate it with all tests from TestCase1 and TestCase2 allSuite = suites['all'] allSuite.addTests( KratosUnittest.TestLoader().loadTestsFromTestCases([ TestCase1, TestCase2 ]) ) # Return the suites return suites # The main function executes the tests if __name__ == '__main__': KratosUnittest.runTests(AssambleTestSuites())
Structure for examples
In order to compute some examples (the examples that used to be located in the folder "test_examples") some additional considerations must to be taken into account. These considerations can be observed in the following example that is located in the StructuralMechanicsApplication/tests. In order to compute a dynamic test (Bossak and Newmark) and a patch test (with bending and membrane behaviours), the following files has been considered, first the main file, which will be the one launched.
# import Kratos from KratosMultiphysics import * from KratosMultiphysics.SolidMechanicsApplication import * from KratosMultiphysics.StructuralMechanicsApplication import * # Import Kratos "wrapper" for unittests import KratosMultiphysics.KratosUnittest as KratosUnittest # Import the tests o test_classes to create the suits from SmallTests import DynamicBossakTests as TDynamicBossakTests from SmallTests import DynamicNewmarkTests as TDynamicNewmarkTests from SmallTests import SprismMembranePatchTests as TSprismMembranePatchTests from SmallTests import SprismBendingPatchTests as TSprismBendingPatchTests def AssambleTestSuites(): Populates the test suites to run. Populates the test suites to run. At least, it should pupulate the suites: "small", "nighlty" and "all" Return ------ suites: A dictionary of suites The set of suites with its test_cases added. suites = KratosUnittest.KratosSuites # Create a test suit with the selected tests (Small tests): smallSuite = suites['small'] smallSuite.addTest(TDynamicBossakTests('test_Bossak')) smallSuite.addTest(TDynamicNewmarkTests('test_Newmark')) smallSuite.addTest(TSprismMembranePatchTests('test_MembranePatch')) smallSuite.addTest(TSprismBendingPatchTests('test_BendingPatch')) # Create a test suit with the selected tests plus all small tests nightSuite = suites['nightly'] nightSuite.addTests(smallSuite) # Create a test suit that contains all the tests: allSuite = suites['all'] allSuite.addTests( KratosUnittest.TestLoader().loadTestsFromTestCases([ TDynamicBossakTests, TDynamicNewmarkTests, TSprismMembranePatchTests, TSprismBendingPatchTests ]) ) return suites if __name__ == '__main__': KratosUnittest.runTests(AssambleTestSuites())
The following can be added as commentary to clarify what the script is doing:
- The tests are added using the string 'test_nametest' from the imported class.
- Nightly should consider at least all the small test, in the same way all should consider all the nightly test.
The following corresponds with the Script that content the test examples:
# Import Kratos from KratosMultiphysics import * # Import KratosUnittest import KratosMultiphysics.KratosUnittest as KratosUnittest import Kratos_Execute_Solid_Test as Execute_Test def GetFilePath(fileName): return os.path.dirname(__file__) + "/" + fileName class DynamicBossakTests(KratosUnittest.TestCase): def setUp(self): self.file_name = "dynamic_test/dynamic_bossak_test" # Initialize GiD I/O input_file_name = GetFilePath(self.file_name) parameter_file = open(self.file_name +"_parameters.json",'r') ProjectParameters = Parameters( parameter_file.read()) # Creating the model part self.test = Execute_Test.Kratos_Execute_Test(ProjectParameters) def test_Bossak(self): self.test.Solve() def tearDown(self): pass class DynamicNewmarkTests(KratosUnittest.TestCase): def setUp(self): self.file_name = "dynamic_test/dynamic_newmark_test" # Initialize GiD I/O input_file_name = GetFilePath(self.file_name) parameter_file = open(self.file_name +"_parameters.json",'r') ProjectParameters = Parameters( parameter_file.read()) # Creating the model part self.test = Execute_Test.Kratos_Execute_Test(ProjectParameters) def test_Newmark(self): self.test.Solve() def tearDown(self): pass class SprismMembranePatchTests(KratosUnittest.TestCase): def setUp(self): self.file_name = "sprism_test/patch_membrane_test" # Initialize GiD I/O input_file_name = GetFilePath(self.file_name) parameter_file = open(self.file_name +"_parameters.json",'r') ProjectParameters = Parameters( parameter_file.read()) # Creating the model part self.test = Execute_Test.Kratos_Execute_Test(ProjectParameters) def test_MembranePatch(self): self.test.Solve() def tearDown(self): pass class SprismBendingPatchTests(KratosUnittest.TestCase): def setUp(self): self.file_name = "sprism_test/patch_bending_test" # Initialize GiD I/O input_file_name = GetFilePath(self.file_name) parameter_file = open(self.file_name +"_parameters.json",'r') ProjectParameters = Parameters( parameter_file.read()) # Creating the model part self.test = Execute_Test.Kratos_Execute_Test(ProjectParameters) def test_BendingPatch(self): self.test.Solve() def tearDown(self): pass
This file calls the main file, that is very similar to the one used by the GiD's problem type:
from __future__ import print_function, absolute_import, division #makes KratosMultiphysics backward compatible with python 2.6 and 2.7 from KratosMultiphysics import * from KratosMultiphysics.SolidMechanicsApplication import * from KratosMultiphysics.StructuralMechanicsApplication import * import json import process_factory class Kratos_Execute_Test: def __init__(self, ProjectParameters): self.ProjectParameters = ProjectParameters self.main_model_part = ModelPart(self.ProjectParameters["problem_data"]["model_part_name"].GetString()) self.main_model_part.ProcessInfo.SetValue(DOMAIN_SIZE, self.ProjectParameters["problem_data"]["domain_size"].GetInt()) self.Model = {self.ProjectParameters["problem_data"]["model_part_name"].GetString() : self.main_model_part} #construct the solver (main setting methods are located in the solver_module) solver_module = __import__(self.ProjectParameters["solver_settings"]["solver_type"].GetString()) self.solver = solver_module.CreateSolver(self.main_model_part, self.ProjectParameters["solver_settings"]) #add variables (always before importing the model part) (it must be integrated in the ImportModelPart) # if we integrate it in the model part we cannot use combined solvers self.solver.AddVariables() ### Temporal #self.main_model_part.AddNodalSolutionStepVariable(ALPHA_EAS) #read model_part (note: the buffer_size is set here) (restart can be read here) self.solver.ImportModelPart() #add dofs (always after importing the model part) (it must be integrated in the ImportModelPart) # if we integrate it in the model part we cannot use combined solvers self.solver.AddDofs() #build sub_model_parts or submeshes (rearrange parts for the application of custom processes) ##get the list of the submodel part in the object Model for i in range(self.ProjectParameters["solver_settings"]["processes_sub_model_part_list"].size()): part_name = self.ProjectParameters["solver_settings"]["processes_sub_model_part_list"][i].GetString() self.Model.update({part_name: self.main_model_part.GetSubModelPart(part_name)}) #obtain the list of the processes to be applied self.list_of_processes = process_factory.KratosProcessFactory(self.Model).ConstructListOfProcesses( self.ProjectParameters["constraints_process_list"] ) self.list_of_processes += process_factory.KratosProcessFactory(self.Model).ConstructListOfProcesses( self.ProjectParameters["loads_process_list"] ) self.list_of_processes += process_factory.KratosProcessFactory(self.Model).ConstructListOfProcesses( self.ProjectParameters["list_other_processes"] ) for process in self.list_of_processes: process.ExecuteInitialize() #### START SOLUTION #### self.computing_model_part = self.solver.GetComputeModelPart() #### output settings start #### self.problem_path = os.getcwd() self.problem_name = self.ProjectParameters["problem_data"]["problem_name"].GetString() #### output settings start #### ## Sets strategies, builders, linear solvers, schemes and solving info, and fills the buffer self.solver.Initialize() def Solve(self): for process in self.list_of_processes: process.ExecuteBeforeSolutionLoop() ## Stepping and time settings (get from process info or solving info) #delta time delta_time = self.ProjectParameters["problem_data"]["time_step"].GetDouble() #start step step = 0 #start time time = self.ProjectParameters["problem_data"]["start_time"].GetDouble() #end time end_time = self.ProjectParameters["problem_data"]["end_time"].GetDouble() # solving the problem (time integration) while(time <= end_time): time = time + delta_time step = step + 1 self.main_model_part.CloneTimeStep(time) for process in self.list_of_processes: process.ExecuteInitializeSolutionStep() self.solver.Solve() for process in self.list_of_processes: process.ExecuteFinalizeSolutionStep() for process in self.list_of_processes: process.ExecuteBeforeOutputStep() for process in self.list_of_processes: process.ExecuteAfterOutputStep() for process in self.list_of_processes: process.ExecuteFinalize()
The main is always the same, just the processes from the project parameters inside the json are changed, look for example the json that corresponds to the Bossak dynamic test:
{ "problem_data" : { "problem_name" : "dynamic_test", "model_part_name" : "Structure", "domain_size" : 2, "time_step" : 0.001, "start_time" : 0.001, "end_time" : 1.00, "echo_level" : 0 }, "solver_settings" : { "solver_type" : "solid_mechanics_implicit_dynamic_solver", "echo_level" : 0, "solution_type" : "Dynamic", "time_integration_method" : "Implicit", "scheme_type" : "Bossak", "model_import_settings" : { "input_type" : "mdpa", "input_filename" : "dynamic_test/dynamic_test" }, "line_search" : false, "convergence_criterion" : "Residual_criterion", "displacement_relative_tolerance" : 0.0001, "displacement_absolute_tolerance" : 1e-9, "residual_relative_tolerance" : 0.0001, "residual_absolute_tolerance" : 1e-9, "max_iteration" : 10, "linear_solver_settings" : { "solver_type": "Super LU", "max_iteration": 500, "tolerance": 1e-9, "scaling": false, "verbosity": 1 }, "problem_domain_sub_model_part_list" : ["Parts_Parts_Auto1"], "processes_sub_model_part_list" : ["DISPLACEMENT_Displacement_Auto1"] }, "constraints_process_list" : [ { "implemented_in_file" : "apply_displacement_process", "implemented_in_module" : "KratosMultiphysics.SolidMechanicsApplication", "help" : "", "process_name" : "ApplyDisplacementProcess", "Parameters" : { "mesh_id" : 0, "model_part_name" : "DISPLACEMENT_Displacement_Auto1", "is_fixed_x" : false, "is_fixed_y" : true, "is_fixed_z" : true, "variable_name" : "DISPLACEMENT", "direction" : [0.1, 0.0, 0.0] } } ], "loads_process_list" : [], "list_other_processes" :[ { "implemented_in_file" : "from_json_check_result_process", "implemented_in_module" : "KratosMultiphysics", "help" : "", "process_name" : "FromJsonCheckResultProcess", "Parameters" : { "check_variables" : ["DISPLACEMENT_X","VELOCITY_X","ACCELERATION_X"], "input_file_name" : "dynamic_test/dynamic_bossak_test_results.json", "model_part_name" : "DISPLACEMENT_Displacement_Auto1", "time_frequency" : 0.01 } } ], "print_output_process" : [ { "implemented_in_file" : "json_output_process", "implemented_in_module" : "KratosMultiphysics", "help" : "", "process_name" : "JsonOutputProcess", "Parameters" : { "output_variables" : ["DISPLACEMENT_X","VELOCITY_X","ACCELERATION_X"], "output_file_name" : "dynamic_test/dynamic_bossak_test_results.json", "model_part_name" : "DISPLACEMENT_Displacement_Auto1", "time_frequency" : 0.01 } } ], "check_json_results_process" : [ { "implemented_in_file" : "from_json_check_result_process", "implemented_in_module" : "KratosMultiphysics", "help" : "", "process_name" : "FromJsonCheckResultProcess", "Parameters" : { "check_variables" : ["DISPLACEMENT_X","VELOCITY_X","ACCELERATION_X"], "input_file_name" : "dynamic_test/dynamic_bossak_test_results.json", "model_part_name" : "DISPLACEMENT_Displacement_Auto1", "time_frequency" : 0.01 } } ], "check_analytic_results_process" : [ { "implemented_in_file" : "from_analytic_check_result_process", "implemented_in_module" : "KratosMultiphysics", "help" : "", "process_name" : "FromAnalyticCheckResultProcess", "Parameters" : { "variable_name" : "DISPLACEMENT_X", "mesh_id" : 0, "f(x,y,z,t)=" : "cos(10.0*t)", "model_part_name" : "DISPLACEMENT_Displacement_Auto1", "time_frequency" : 0.01 } } ], "apply_custom_function_process" : [], "restart_options" : { "SaveRestart" : false, "RestartFrequency" : 0, "LoadRestart" : false, "Restart_Step" : 0 }, "constraints_data" : { "incremental_load" : false, "incremental_displacement" : false } }
You can download this example from here[2]
Some interesting comments to add to clarify:
- The solver, constitutive laws, etc..., everything is created from the main file, that should be ALWAYS the same, and just change the processes, or even include your local processes, but never change the main.
- To be recognized all the test need to have the name "test_" at the beginning of the definition ("test_MembranePacth" and "test_BendingPacth" in this example).
- The "tearDown" doesn't do anything, it just close files, clear memory...
- "imports" should be at the beginning of the file to avoid conflicts
- NO PRINTS, is not necessary, the unittest already prints all the necessary information.
Some common commands in Unittest
The link[3] contents the commands that can be used with the original Unittest from Python, not all the commands from the python Unittest are available in Kratos right now, but they will be available in a near future. The following are the most common and essential commands in Kratos Unittest:
- assertTrue/assertFalse:They check if the two inputs are true or false respectively.
- assertEqual: Check if the two inputs are exactly equal, if the inputs are doubles it is recommended to use the next assert, in order to avoid precision errors.
- assertAlmostEqual: Check if the solution is almost equal in the two inputs, with a certain precision (check the unittest python wiki for more details[4])