Repository: chris-greening/spyrograph (language: python)

Issue:
Add pytest framework in place
**Is your feature request related to a problem? Please describe.**
Add `pytest` automated testing framework in place for proper unit testing moving forward


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml
index e28c3a0..772da58 100644
--- a/.github/workflows/pylint.yaml
+++ b/.github/workflows/pylint.yaml
@@ -21,4 +21,4 @@ jobs:
         pip install numpy
     - name: Analysing the code with pylint
       run: |
-        pylint $(git ls-files '*.py')
\ No newline at end of file
+        pylint $(git ls-files 'spyrograph/*.py')
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
index 35d3330..06dc518 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -57,7 +57,7 @@ ignore-paths=
 # Files or directories matching the regular expression patterns are skipped.
 # The regex matches against base names, not paths. The default value ignores
 # Emacs file locks
-ignore-patterns=^\.#
+ignore-patterns=^\.#,tests
 
 # List of module names for which member attributes should not be checked
 # (useful for modules/projects where namespaces are manipulated during runtime


Test patch:
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
new file mode 100644
index 0000000..e77ffd0
--- /dev/null
+++ b/.github/workflows/pytest.yaml
@@ -0,0 +1,23 @@
+name: Pytest
+
+on: [pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.10"]
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v3
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install pytest pandas numpy
+    - name: Unit testing the code with pytest
+      run: |
+        pytest tests
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/_roulette.py b/tests/_roulette.py
new file mode 100644
index 0000000..f840c14
--- /dev/null
+++ b/tests/_roulette.py
@@ -0,0 +1,123 @@
+import pytest
+
+import numpy as np
+import pandas as pd
+
+class _TestGeneral:
+    # Define this class attr in subclasses
+    class_name = None
+    @pytest.fixture()
+    def thetas(self) -> "np.array":
+        """Return a numpy array of theta values"""
+        return np.arange(0, np.pi*2, .1)
+
+    @pytest.fixture()
+    def instance(self, thetas):
+        """Return an instance of the shape"""
+        return self.class_name(
+            R = 300,
+            r = 200,
+            d = 100,
+            thetas=thetas
+        )
+
+    def test_theta_range(self) -> None:
+        """Test that passing theta start, stop, step creates an expected list of theta values"""
+        obj = self.class_name(
+            R = 300,
+            r = 200,
+            d = 100,
+            theta_start=1,
+            theta_stop=10,
+            theta_step=1
+        )
+        assert all(obj.thetas == np.arange(1,10,1))
+
+    def test_theta_range_default_step(self) -> None:
+        """Test that passing theta start, stop, step creates an expected list of theta values"""
+        obj = self.class_name(
+            R = 300,
+            r = 200,
+            d = 100,
+            theta_start=0,
+            theta_stop=1
+        )
+
+    def test_multiple_thetas_exception(self, thetas: "np.array") -> None:
+        """Test that passing multiple definitions of setting theta raises a ValueError"""
+        with pytest.raises(ValueError):
+            obj = self.class_name(
+                R = 300,
+                r = 200,
+                d = 100,
+                thetas=thetas,
+                theta_start=1,
+                theta_stop=10,
+                theta_step=1
+            )
+
+    def test_coords(self, instance) -> None:
+        """Test that coordinates attribute matches x, y, and theta values"""
+        assert instance.coords[0] == (instance.x[0], instance.y[0], instance.thetas[0])
+        assert instance.coords[-1] == (instance.x[-1], instance.y[-1], instance.thetas[-1])
+
+    def test_dataframe_property(self, instance) -> None:
+        """Test that DataFrame property is working as expected"""
+        assert isinstance(instance.df, pd.DataFrame)
+        assert list(instance.df.columns) == ["x", "y", "theta"]
+        assert all(instance.df["x"].to_numpy() == instance.x)
+        assert all(instance.df["y"].to_numpy() == instance.y)
+        assert all(instance.df["theta"].to_numpy() == instance.thetas)
+
+class _TestSpecial:
+    # Define this class attr in subclasses
+    class_name = None
+    @pytest.fixture()
+    def thetas(self) -> "np.array":
+        """Return a numpy array of theta values"""
+        return np.arange(0, np.pi*2, .1)
+
+    @pytest.fixture()
+    def instance(self, thetas):
+        """Return an instance of the shape"""
+        return self.class_name(
+            R = 300,
+            r = 200,
+            thetas=thetas
+        )
+
+    def test_theta_range(self) -> None:
+        """Test that passing theta start, stop, step creates an expected list of theta values"""
+        obj = self.class_name(
+            R = 300,
+            r = 200,
+            theta_start=1,
+            theta_stop=10,
+            theta_step=1
+        )
+        assert all(obj.thetas == np.arange(1,10,1))
+
+    def test_theta_range_default_step(self) -> None:
+        """Test that passing theta start, stop, step creates an expected list of theta values"""
+        obj = self.class_name(
+            R = 300,
+            r = 200,
+            theta_start=0,
+            theta_stop=1
+        )
+
+    def test_multiple_thetas_exception(self, thetas: "np.array") -> None:
+        """Test that passing multiple definitions of setting theta raises a ValueError"""
+        with pytest.raises(ValueError):
+            obj = self.class_name(
+                R = 300,
+                r = 200,
+                thetas=thetas,
+                theta_start=1,
+                theta_stop=10,
+                theta_step=1
+            )
+
+    def test_rolling_radius_equals_distance(self, instance):
+        """Test radius of rolling circle is equal to distance"""
+        assert instance.r == instance.d
diff --git a/tests/test_epitrochoid.py b/tests/test_epitrochoid.py
new file mode 100644
index 0000000..3434e55
--- /dev/null
+++ b/tests/test_epitrochoid.py
@@ -0,0 +1,14 @@
+import pytest
+
+from tests._roulette import _TestGeneral, _TestSpecial
+from spyrograph.epitrochoid.epitrochoid import Epitrochoid
+from spyrograph.epitrochoid.epicycloid import Epicycloid
+
+class TestEpitrochoid(_TestGeneral):
+    class_name = Epitrochoid
+
+    def test_circle_offset(self, instance):
+        assert instance._circle_offset() == 500
+
+class TestEpicycloid(_TestSpecial, TestEpitrochoid):
+    class_name = Epicycloid
diff --git a/tests/test_hypotrochoid.py b/tests/test_hypotrochoid.py
new file mode 100644
index 0000000..5133270
--- /dev/null
+++ b/tests/test_hypotrochoid.py
@@ -0,0 +1,15 @@
+import numpy as np
+import pytest
+
+from tests._roulette import _TestGeneral, _TestSpecial
+from spyrograph.hypotrochoid.hypotrochoid import Hypotrochoid
+from spyrograph.hypotrochoid.hypocycloid import Hypocycloid
+
+class TestHypotrochoid(_TestGeneral):
+    class_name = Hypotrochoid
+
+    def test_circle_offset(self, instance):
+        assert instance._circle_offset() == 100
+
+class TestHypocycloid(_TestSpecial, TestHypotrochoid):
+    class_name = Hypocycloid