class TestHelloWorldThings(unittest.TestCase): """Basic non-brokenness tests.""" @freeze_time(datetime.datetime.now()) def setUp(self): # Blank out the test JSON files: data_map = { # todo DRY, this is repeated in every model interface's tests module "user_data": "test/testing_mockUserDB.json", "datespot_data": "test/testing_mockDatespotDB.json", "match_data": "test/testing_mockMatchData.json", "review_data": "test/testing_mockReviewData.json", "message_data": "test/testing_mockMessageData.json", "chat_data": "test/testing_mockChatData.json" } with open(TEST_JSON_DB_NAME, 'w') as fobj: json.dump(data_map, fobj) fobj.seek(0) # make sure all the test-mock JSONs exist for filename in data_map: with open(data_map[filename], 'w') as fobj: json.dump({}, fobj) fobj.seek(0) # Instantiate DatabaseAPI object and model interfaces self.db = DatabaseAPI(json_map_filename=TEST_JSON_DB_NAME) self.user_data = model_interfaces.UserModelInterface( json_map_filename=TEST_JSON_DB_NAME ) # These are cumbersome, but no implementation code actually needs to ask the DB API for self.datespot_data = model_interfaces.DatespotModelInterface( json_map_filename=TEST_JSON_DB_NAME ) # ...a model object. DB API having a "get_object()" method would be convenient self.match_data = model_interfaces.MatchModelInterface( json_map_filename=TEST_JSON_DB_NAME ) # ...for the tests code but have no other use. self.review_data = model_interfaces.ReviewModelInterface( json_map_filename=TEST_JSON_DB_NAME) self.chat_data = model_interfaces.ChatModelInterface( json_map_filename=TEST_JSON_DB_NAME) self.message_data = model_interfaces.MessageModelInterface( json_map_filename=TEST_JSON_DB_NAME) # Data for mock users self.azura_name = "Azura" self.azura_location = (40.73517750328247, -74.00683227856715) self.azura_id = "1" self.azura_existing_taste_name = "dawn" self.azura_existing_taste_strength = 0.9 self.azura_existing_taste_datapoints = 3 self.azura_existing_tastes = { # This is the data in the object's internal format, for testing convenience not for a call to the external create_user() self.azura_existing_taste_name: [ self.azura_existing_taste_strength, self.azura_existing_taste_datapoints ] } self.azura_data = { "name": self.azura_name, "current_location": self.azura_location, "force_key": self.azura_id } self.boethiah_name = "Boethiah" self.boethiah_location = (40.76346250260515, -73.98013893542904) self.boethiah_id = "2" self.boethiah_data = { "name": self.boethiah_name, "current_location": self.boethiah_location, "force_key": self.boethiah_id } self.hircine_name = "Hircine" self.hircine_location = (40.76525023033338, -73.96722141608099) self.hircine_id = "3" self.hircine_data = { "name": self.hircine_name, "current_location": self.hircine_location, "force_key": self.hircine_id } # Data for mock Datespot self.terrezanos_location = (40.737291166191476, -74.00704685527774) self.terrezanos_name = "Terrezano's" self.terrezanos_traits = { "italian": [1.0, "discrete"], "wine": [0.5, 1], "pasta": [0.6, 2], "NOT FROM PIZZA HUT": [0.01, 2], "authentic": [-0.05, 3], "warehouse": [1.0, "discrete"] } self.terrezanos_price_range = 2 self.terrezanos_hours = [ [14, 22], [14, 21], [14, 21], [14, 21], [14, 23], [14, 23], [14, 20] ] # ints in [0..23] representing hours, for now self.terrezanos_data = { "location": self.terrezanos_location, "name": self.terrezanos_name, "traits": self.terrezanos_traits, "price_range": self.terrezanos_price_range, "hours": self.terrezanos_hours, } self.terrezanos_id = self.db.post_object({ "object_model_name": "datespot", "object_data": self.terrezanos_data }) # Data for mock Review of Terrezano's self.mock_text_positive_relevant = "This was a wonderful place to go on a date. I had the pasta. It was authentic and not from Pizza Hut." self.expected_sentiment = 0.1906 # todo hardcoded self.expected_relevance = round(1 / len(self.mock_text_positive_relevant), 4) # i.e. "date" appears once. self.terrezanos_review_data = { "datespot_id": self.terrezanos_id, "text": self.mock_text_positive_relevant } # Add three users for use in testing compound objects self.db.post_object({ "object_model_name": "user", "object_data": self.azura_data }) self.db.post_object({ "object_model_name": "user", "object_data": self.boethiah_data }) self.db.post_object({ "object_model_name": "user", "object_data": self.hircine_data }) # Data for mock Message and Chat self.mock_bilateral_timestamp = time.time() self.quick_mock_chat_data = { "start_time": time.time(), "participant_ids": [self.azura_id, self.boethiah_id] } self.mock_chat_id_1 = self.db.post_object({ "object_model_name": "chat", "object_data": self.quick_mock_chat_data }) # Need a Chat to create a Message self.single_sentence_text = "Worship the Nine, do your duty, and heed the commands of the saints and priests." self.expected_sentiment_single_sentence = 0.296 # todo hardcoded self.mock_bilateral_message_data = { "time_sent": self.mock_bilateral_timestamp, "sender_id": self.azura_id, "chat_id": self.mock_chat_id_1, "text": self.single_sentence_text } # Add two matches for Azura user @freeze_time("2021-05-01 12:00:01") def freezetime_match_1(match_data: dict) -> str: """Creates the match at a specified timestamp to avoid unittest forcing the timestamps to be identical; returns the id string.""" return self.db.post_object(match_data) @freeze_time("2021-05-01 12:00:02") # One second later def freezetime_match_2(match_data: dict) -> str: """Creates the second match at a later timestamp.""" return self.db.post_object(match_data) match_data_azura_boethiah = { "object_model_name": "match", "object_data": { "user1_id": self.azura_id, "user2_id": self.boethiah_id } } match_data_hircine_azura = { "object_model_name": "match", "object_data": { "user1_id": self.hircine_id, "user2_id": self.azura_id } } self.match_id_azura_boethiah = freezetime_match_1( match_data_azura_boethiah) self.match_id_hircine_azura = freezetime_match_2( match_data_hircine_azura) self.match_obj_azura_boethiah = self.match_data.lookup_obj( self.match_id_azura_boethiah) self.match_obj_hircine_azura = self.match_data.lookup_obj( self.match_id_hircine_azura) def test_init(self): """Was an object of the expected type instantiated?""" self.assertIsInstance(self.db, DatabaseAPI) def test_model_interface_constructor_calls(self): """Does the DB interface call the expected model interface for each model name?""" expected_interfaces = { "user": model_interfaces.UserModelInterface, "datespot": model_interfaces.DatespotModelInterface, "match": model_interfaces.MatchModelInterface, "review": model_interfaces.ReviewModelInterface, "message": model_interfaces.MessageModelInterface, "chat": model_interfaces.ChatModelInterface } for model_name in expected_interfaces: actual_interface = self.db._model_interface(model_name) self.assertIsInstance(actual_interface, expected_interfaces[model_name]) def test_validate_model_name(self): """Does the validator raise the expected error for a bad model name?""" with self.assertRaises(ValueError): self.db._validate_model_name("foo") ### Tests for post_object() and get_object() ### def test_post_obj_user(self): talos_name = "Talos" talos_location = (40.76346250260515, -73.98013893542904) expected_talos_id = "4" talos_data = { "name": talos_name, "current_location": talos_location, "force_key": expected_talos_id } actual_talos_id = self.db.post_object({ "object_model_name": "user", "object_data": talos_data }) self.assertIsInstance(actual_talos_id, str) talos_obj = self.user_data.lookup_obj(actual_talos_id) self.assertIsInstance(talos_obj, models.User) self.assertEqual(expected_talos_id, actual_talos_id) with self.assertRaises( ValueError ): # Trying with key already in DB should raise error talos_data["force_key"] = self.hircine_id # Used in setUp actual_talos_id = self.db.post_object({ "object_model_name": "user", "object_data": talos_data }) def test_post_obj_datespot(self): domenicos_location = (40.723889184134926, -73.97613846772394) domenicos_name = "Domenico's" domenicos_traits = { "coffee": [ 1.0, 1 ], # todo...So we're imagining this as ~ how good the coffee is, rather than the discrete fact that they do serve coffee? "coffee shop": [1.0, "discrete"], "gourmet": [0.25, 1], "americano": [0.15, 1], "knows coffee": [0.3, 1], "bricks": [0.6, 1], "burger juice": [0.9, 1] } domenicos_price_range = 1 domenicos_hours = [[8, 19], [8, 19], [8, 19], [8, 19], [8, 19], [8, 19], [10, 17]] domenicos_data = { "location": domenicos_location, "name": domenicos_name, "traits": domenicos_traits, "price_range": domenicos_price_range, "hours": domenicos_hours } domenicos_id = self.db.post_object({ "object_model_name": "datespot", "object_data": domenicos_data }) domenicos_obj = self.datespot_data.lookup_obj(domenicos_id) self.assertIsInstance(domenicos_obj, models.Datespot) def test_post_obj_match(self): match_data = {"user1_id": self.azura_id, "user2_id": self.boethiah_id} match_id = self.db.post_object({ "object_model_name": "match", "object_data": match_data }) match_obj = self.match_data.lookup_obj(match_id) self.assertIsInstance(match_obj, models.Match) def test_post_obj_review(self): review_id = self.db.post_object({ "object_model_name": "review", "object_data": self.terrezanos_review_data }) review_obj = self.review_data.lookup_obj(review_id) self.assertIsInstance(review_obj, models.Review) def test_post_obj_message(self): message_id = self.db.post_object({ "object_model_name": "message", "object_data": self.mock_bilateral_message_data }) message_obj = self.message_data.lookup_obj(message_id) self.assertIsInstance(message_obj, models.Message) def test_post_obj_chat(self): chat_id = self.db.post_object({ "object_model_name": "chat", "object_data": self.quick_mock_chat_data }) chat_obj = self.chat_data.lookup_obj(chat_id) self.assertIsInstance(chat_obj, models.Chat) ### Tests for put_json() ### def test_put_json_update_user(self): # Test updating a User's location: new_data = { "current_location": (40.737291166191476, -74.00704685527774), } args_data = { "object_model_name": "user", "object_id": self.azura_id, "update_data": new_data } self.db.put_data(args_data) azura_obj = self.user_data.lookup_obj(self.azura_id) self.assertAlmostEqual(new_data["current_location"], azura_obj.current_location) def test_updating_unsupported_model_raises_error(self): """Does attempting to update a model for which updates aren't supported raise the expected error?""" unsupported_models = ["review", "message"] arbitrary_object_id = "a" arbitrary_data = {"foo": "bar"} for model in unsupported_models: with self.assertRaises(ValueError): self.db.put_data({ "object_model_name": model, "object_id": arbitrary_object_id, "update_data": arbitrary_data }) # TODO complete for other models and their main anticipated update cases ### Tests for post_decision() ### def test_post_yes_decision_no_match(self): """Does posting a "yes" decision that doesn't create a match return the expected JSON?""" decision_yes_data = { "user_id": self.azura_id, "candidate_id": self.boethiah_id, "outcome": True } expected_response_data = {"match_created": False} actual_response_data = self.db.post_decision(decision_yes_data) self.assertEqual(expected_response_data, actual_response_data) def test_post_yes_decision_yes_match(self): """Does posting a "yes" decision that creates a match return the expected JSON?""" # Post a decision of Azura liking Boethiah: azura_decision_yes_data = { "user_id": self.azura_id, "candidate_id": self.boethiah_id, "outcome": True } self.db.post_decision(azura_decision_yes_data) # Post a second decision of Boethiah liking Azura boethiah_decision_yes_data = { "user_id": self.boethiah_id, "candidate_id": self.azura_id, "outcome": True } expected_response_data = {"match_created": True} actual_response_data = self.db.post_decision( boethiah_decision_yes_data) self.assertEqual(expected_response_data, actual_response_data) def test_post_no_decision(self): """Does posting a "no" decision return the expected JSON?""" decision_no_data = { "user_id": self.azura_id, "candidate_id": self.boethiah_id, "outcome": False } expected_response_data = {"match_created": False} actual_response_data = self.db.post_decision(decision_no_data) self.assertEqual(expected_response_data, actual_response_data) def test_non_boolean_outcome_raises_error(self): """Does posting JSON without a boolean outcome raise the expected error?""" bad_data = { "user_id": self.azura_id, "candidate_id": self.boethiah_id, "outcome": 2 } with self.assertRaises(TypeError): self.db.post_decision(bad_data) ### Tests for get_datespots_near() ### # Code that makes live API calls isn't covered by the main tests suite def test_get_datespots_near_cache_only_default_radius(self): """Does the method return a string matching the expected shape of a JSON-ified list of Datespots in response to a query that provides valid location but no radius?""" location_query_data = { "location": (40.737291166191476, -74.00704685527774) } results = self.db.get_datespots_near(location_query_data) self.assertIsInstance(results, list) self.assertGreater(len(results), 0) first_result = results[0] distance, datespot = first_result # it's a two-element tuple self.assertIsInstance(distance, float) self.assertIsInstance(datespot, models.Datespot) def test_get_datespots_near_cache_only_nondefault_radius(self): """Does the method return the expected JSON in response to a query that provides valid location and specifies a non-default_radius?""" location_query_data = { "location": (40.737291166191476, -74.00704685527774), "radius": 4000 } results = self.db.get_datespots_near(location_query_data) self.assertIsInstance(results, list) self.assertGreater(len(results), 0) first_result = results[0] distance, datespot = first_result # it's a two-element tuple self.assertIsInstance(distance, float) self.assertIsInstance(datespot, models.Datespot) ### Tests for get_datespot_suggestions() ### def test_get_candidate_datespots(self): """Does the method return the expected JSON in response to JSON matching with a valid Match?""" # Put a Match in the mock DB match_data = {"user1_id": self.azura_id, "user2_id": self.boethiah_id} match_id = self.db.post_object({ "object_model_name": "match", "object_data": match_data }) match_obj = self.match_data.lookup_obj(match_id) query_data = {"match_id": match_id} results = self.db.get_candidate_datespots(query_data) self.assertIsInstance(results, list) self.assertGreater(len(results), 0) # Terrezanos should be the only Datespot known to the DB here: self.assertEqual(results[0][1].id, self.terrezanos_id) ### Tests for other public methods ### def test_get_next_candidate( self ): # We have two Users in the DB, so one will be the other's candidate query_data = {"user_id": self.azura_id} result = self.db.get_next_candidate(query_data) candidate_name = result["name"] self.assertEqual(self.boethiah_name, candidate_name) # TODO Post / get obj / get json for all of: # user # datespot # match # review # message # chat def test_get_matches_list(self): expected_result_data = [ { # Expecte the Match created second to appear first in the list "match_id": self.match_id_hircine_azura, "match_timestamp": self.match_obj_hircine_azura.timestamp, "match_partner_info": self.user_data.render_candidate(self.hircine_id) }, { "match_id": self.match_id_azura_boethiah, "match_timestamp": self.match_obj_hircine_azura.timestamp, "match_partner_info": self.user_data.render_candidate(self.boethiah_id) } ] actual_result_data = self.db.get_matches_list( query_data={"user_id": self.azura_id}) #self.assertEqual(actual_result_data, expected_result_data) # TODO Had to give up for now on asserting about the timestamps due to weird behavior--keep getting them created with identical timestamps # when created in setUp, even though the timestamps increment in match.py. Unittests for match.py confirmed that the underlying sort works. for result in actual_result_data: self.assertIsInstance(result, dict) self.assertEqual(len(result), len(expected_result_data[0])) def test_get_suggestions(self): expected_result_data = [ # Terrezanos is the only Datespot in the DB here self.datespot_data.render_obj(self.terrezanos_id) ] actual_result_data = self.db.get_suggestions_list( {"match_id": self.match_id_azura_boethiah}) self.assertEqual(actual_result_data, expected_result_data)
class TestHelloWorldThings(unittest.TestCase): def setUp(self): self.db = DatabaseAPI() # Testing on the real DB, to have restaurants # TODO: script that populates the test DBs with realistic restaurants en masse. And/or separate JSON map for this test # (pointing to same test DB filenames for some like users, different one for datespots) self.user_data = model_interfaces.UserModelInterface() # Need user objects to instantiate a Match grortName = "Grort" self.grortCurrentLocation = (40.746667, -74.001111) grort_data = { "name": grortName, "current_location": self.grortCurrentLocation } self.grort_user_id = self.db.post_object({ "object_model_name": "user", "object_data": grort_data }) self.userGrort = self.user_data.lookup_obj(self.grort_user_id) drobbName = "Drobb" self.drobbCurrentLocation = (40.767376158866554, -73.98615327558278) drobb_data = { "name": drobbName, "current_location": self.drobbCurrentLocation } self.drobb_user_id = self.db.post_object({ "object_model_name": "user", "object_data": drobb_data }) self.userDrobb = self.user_data.lookup_obj(self.drobb_user_id) # distance should be approx 2610m # midpoint should be circa (40.75827478958617, -73.99310556132602) self.matchGrortDrobb = models.Match(self.userGrort, self.userDrobb) assert self.matchGrortDrobb.midpoint is not None # Get the candidates list that the DatabaseAPI would be giving to Match: self.candidate_datespots_list = self.db.get_datespots_near( {"location": self.matchGrortDrobb.midpoint}) def test_hash(self): """Does the __hash__() method's return value match the value obtained by mimicking its logic in the test code?""" expected_hash = hash((self.grort_user_id, self.drobb_user_id)) actual_hash = hash(self.matchGrortDrobb) self.assertEqual(actual_hash, expected_hash) def test_hash_output_consistent_regardless_of_user_order(self): """Does Match(Alice, Bob) hash to same value as Match(Bob, Alice)?""" expected_hash = hash(self.matchGrortDrobb) match_obj_flipped_members = models.Match( self.userDrobb, self.userGrort ) # Reverse the user1 and user2 roles from those in setUp assert match_obj_flipped_members.user1 == self.matchGrortDrobb.user2 and match_obj_flipped_members.user2 == self.matchGrortDrobb.user1 actual_hash = hash(match_obj_flipped_members) self.assertEqual(actual_hash, expected_hash) # Test same for the public id property attribute expected_id = self.matchGrortDrobb.id actual_id = match_obj_flipped_members.id self.assertEqual(actual_id, expected_id) # TODO The implementation code isn't correct as of 6/10. Observed same user ID strings producing differnent Match id hashes # during Postman endpoint testing. def test_public_id_attribute_matches_hash(self): """Does the public Match.id attribute-property bear the expected relationship to the return value Match.__hash__()?""" expected_id = str(hex(hash(self.matchGrortDrobb)))[ 2:] # Mimic logic in Match._id() private method actual_id = self.matchGrortDrobb.id self.assertEqual(actual_id, expected_id) def test_compute_midpoint(self): maxDelta = 0.01 approxExpectedMidpoint = (40.75827478958617, -73.99310556132602) expectedLat, expectedLon = approxExpectedMidpoint actualLat, actualLon = self.matchGrortDrobb.midpoint self.assertAlmostEqual(actualLat, expectedLat, delta=expectedLat * maxDelta) self.assertAlmostEqual(actualLon, expectedLon, delta=expectedLat * maxDelta) def test_public_distance_attribute(self): """Does the public distance attribute-property return the expected distance?""" expected_distance = geo_utils.haversine(self.grortCurrentLocation, self.drobbCurrentLocation) actual_distance = self.matchGrortDrobb.distance self.assertAlmostEqual(actual_distance, expected_distance) def test_get_suggestions_return_type(self): """Does Match.get_suggestions() external method return the expected type?""" expected_return_type = list returned_obj = self.matchGrortDrobb.suggestions( self.candidate_datespots_list) self.assertIsInstance(returned_obj, expected_return_type) def test_get_suggestions_return_not_null(self): returned_obj = self.matchGrortDrobb.suggestions( self.candidate_datespots_list) self.assertGreater(len(returned_obj), 0) def test_get_suggestions_return_shape(self): """Does the returned object's shape (nested lists/tuples) match the expected structure?""" returned_obj = self.matchGrortDrobb.suggestions( self.candidate_datespots_list) # each "suggestion" should be a Datespot object literal: for element in returned_obj: self.assertIsInstance(element, models.Datespot) # def test_db_user_method_returns_expected_query_results(self): # """Does the method that calls the main database API return the expected query results, i.e. # a list of serialized restaurant object dicts sorted by distance?""" # returned_obj = self.matchGrortDrobb._get_datespots_by_geography() # #print(returned_obj) # self.assertIsInstance(returned_obj, list) # self.assertGreater(len(returned_obj), 0) def test_internal_scorer_method_returns_expected_data(self): """Does the internal "private" method responsible for scoring each nearby datespot return a non-empty list?""" returned_obj = self.matchGrortDrobb._score_nearby_datespots( self.candidate_datespots_list) self.assertIsInstance(returned_obj, list) self.assertGreater(len(returned_obj), 0)