Skip to content

Commit

Permalink
implement unified testing approach
Browse files Browse the repository at this point in the history
(see README)
Enables a way for tests to be written once in python. They can be run in pytest (with bin/run_pytest) or in jest (with bin/run_jest, which compiles the py test suites into JS, then jest runs on the output JS)

Add tests for base_modes and emcommon.util as the first examples of how this will work.

The existing tests that already have separate .js files can be kept how they are.
  • Loading branch information
JGreenlee committed Aug 27, 2024
1 parent 324dab0 commit dae9c9d
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__/
e_mission_common.egg-info/
node_modules/
setup/
test_js*
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,26 @@ For JavaScript:
1. Run `bash bin/compile_to_js.sh` to build the JavaScript.
1. From this repo, run `npm link` to establish a symlink to your local version of e-mission-common.
1. From the other repo, run `npm link e-mission-common` to use the symlinked version of this repo.

## Unit testing

Due to the nature of this library, it is critical to test both the Python source and the compiled JavaScript. Ideally, we can write a test suite in a `.py` file and have it run in both Python and JavaScript environments.

This is possible, to a degree, using `transcrypt` to compile test files to JavaScript, and then running `jest` on the compiled JavaScript.
> `pytest` was chosen over `unittest` because it is more flexible, allowing tests to be written in a way that can be compiled to Jest-compatible JavaScript. (It is also more concise and friendly to write.)

See the `tests` directory for `.py` files that are not accompanied by a `.js` file. These files are intended to be run in both Python and JavaScript environments.

### Dedicated JS test files

There may be testing scenarios that must significantly diverge between Python and JavaScript versions. In these cases, we can write a separate `.js` file next to the `.py` file of the same name. In this case, no compilation is necessary; `pytest` will run the `.py` file and `jest` will run the `.js`.

### Running the tests

```bash
. bin/run_pytest.sh
```

```bash
. bin/run_jest.sh
```
1 change: 1 addition & 0 deletions bin/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies:
- pip
- python=3.9
- pytest
- pytest-asyncio
- requests
- pip:
- Transcrypt==3.9.3
25 changes: 25 additions & 0 deletions bin/run_jest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

. setup/activate_conda.sh && conda activate emcommon

echo "Running JavaScript tests..."

ROOT_DIR_ABSOLUTE=$(realpath)

# remove any existing test_js_* directories (from previous runs)
rm -rf $ROOT_DIR_ABSOLUTE/test_js_*


for file in test/**/test_*.py; do
# if there is a .js file in the same directory, skip
if [ -f "${file%.*}.js" ]; then
echo "Found dedicated .js version of test $file, not compiling"
continue
else
echo "Compiling $file"
fi
# filename without directory and extension
suffix=$(basename $file)
PYTHONPATH=./src transcrypt --nomin --ecom --xreex $file --xpath test -od $ROOT_DIR_ABSOLUTE/test_js_$suffix
done
npx node --experimental-vm-modules node_modules/jest/bin/jest.js
5 changes: 5 additions & 0 deletions bin/run_pytest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

. setup/activate_conda.sh && conda activate emcommon

PYTHONPATH=./src pytest test --asyncio-mode=auto
6 changes: 0 additions & 6 deletions bin/run_tests.sh

This file was deleted.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
"description": "A common library to share code between projects in the e-mission-platform. Written in Python and exposed as both Python and JavaScript.",
"main": "./emcommon_js/index.js",
"types": "./emcommon_js/dist/index.d.ts",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"jest": {
"testMatch": [
"**/test/**/*.js"
"**/test_*.js"
],
"globals": { "window": {} },
"transform": {}
Expand Down
Empty file added test/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions test/__testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
def _expect(condition):
assert condition # __: skip
'''?
expect(condition).toBeTruthy()
?'''


def expectEqual(a, b):
assert a == b # __: skip
'''?
expect(a).toBe(b)
?'''


def expectAlmostEqual(a, b, delta=0.001):
assert abs(a - b) < delta # __: skip
'''?
expect(a).toBeCloseTo(b, delta)
?'''


def jest_test(test):
'''?
window['jest_tests'] = window['jest_tests'] or []
window['jest_tests'].append(test)
?'''
return test


def jest_describe(describe_name):
'''?
tests = window['jest_tests'] or []
expect(tests.length).toBeGreaterThan(0)
def run_tests():
for t in tests:
test(t.js_name, t)
describe(describe_name, run_tests);
?'''
pass
Empty file added test/diary/__init__.py
Empty file.
73 changes: 73 additions & 0 deletions test/diary/test_base_modes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# __pragma__('jsiter')

import emcommon.diary.base_modes as emcdb
from ..__testing import jest_test, jest_describe, expectEqual


@jest_test
def test_get_base_mode_by_key():
base_mode = emcdb.get_base_mode_by_key("BUS")
expected_base_mode = {
"icon": 'bus-side',
"color": emcdb.mode_colors['magenta'],
"met": emcdb.NON_ACTIVE_METS,
"footprint": {"transit": ["MB", "RB", "CB"]},
}
for key in expected_base_mode:
expectEqual(str(base_mode[key]), str(expected_base_mode[key]))


@jest_test
def test_get_rich_mode_car():
fake_label_option = {'base_mode': "CAR", 'passengers': 2}
rich_mode = emcdb.get_rich_mode(fake_label_option)

expected_rich_mode = {
"base_mode": "CAR",
"passengers": 2,
"icon": 'car',
"color": emcdb.mode_colors['red'],
"met": emcdb.NON_ACTIVE_METS,
"footprint": emcdb.CAR_FOOTPRINT,
}
for key in expected_rich_mode:
expectEqual(str(rich_mode[key]), str(expected_rich_mode[key]))

# ensure the original object was not mutated
expectEqual(str(fake_label_option), str({'base_mode': "CAR", 'passengers': 2}))


@jest_test
def test_get_rich_mode_e_car():
fake_label_option = {
'baseMode': "E_CAR", # with baseMode as camelCase, should still work
'passengers': 1,
'color': '#000000',
'footprint': {'gasoline': {'wh_per_km': 500, 'weight': 1}}
}
rich_mode = emcdb.get_rich_mode(fake_label_option)

expected_rich_mode = {
"baseMode": "E_CAR",
"passengers": 1,
"color": '#000000',
"footprint": {'gasoline': {'wh_per_km': 500, 'weight': 1}},
"icon": 'car-electric',
"met": emcdb.NON_ACTIVE_METS,
}
for key in expected_rich_mode:
expectEqual(str(rich_mode[key]), str(expected_rich_mode[key]))

# ensure the original object was not mutated
expectEqual(str(fake_label_option),
str({
'baseMode': "E_CAR",
'passengers': 1,
'color': '#000000',
'footprint': {'gasoline': {'wh_per_km': 500, 'weight': 1}}
}))


jest_describe('test_base_modes')

# __pragma__('nojsiter')
21 changes: 21 additions & 0 deletions test/test_emcommon_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import emcommon.logger as Log
import emcommon.util as emcu
from .__testing import jest_test, _expect as expect, jest_describe


@jest_test
async def test_read_json_resource():
result = await emcu.read_json_resource('label-options.default.json')
# result['MODE'] should contain a mode with value 'taxi'
expect(any([mode['value'] == 'taxi' for mode in result['MODE']]))


@jest_test
async def test_fetch_url():
url = "https://jsonplaceholder.typicode.com/posts/1"
result = await emcu.fetch_url(url)
expect(result['title'] ==
'sunt aut facere repellat provident occaecati excepturi optio reprehenderit')


jest_describe('test_example')

0 comments on commit dae9c9d

Please sign in to comment.