Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 108 additions & 35 deletions piccolo/testing/model_builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

import datetime
import inspect
import json
import typing as t
from decimal import Decimal
from uuid import UUID
from functools import partial
from types import MappingProxyType

from piccolo.columns import JSON, JSONB, Array, Column, ForeignKey
from piccolo.custom_types import TableInstance
Expand All @@ -13,18 +15,8 @@


class ModelBuilder:
__DEFAULT_MAPPER: t.Dict[t.Type, t.Callable] = {
bool: RandomBuilder.next_bool,
bytes: RandomBuilder.next_bytes,
datetime.date: RandomBuilder.next_date,
datetime.datetime: RandomBuilder.next_datetime,
float: RandomBuilder.next_float,
int: RandomBuilder.next_int,
str: RandomBuilder.next_str,
datetime.time: RandomBuilder.next_time,
datetime.timedelta: RandomBuilder.next_timedelta,
UUID: RandomBuilder.next_uuid,
}
__DEFAULT_MAPPER: t.Dict[t.Type, t.Callable] = {}
__OTHER_MAPPER: t.Dict[t.Type, t.Callable] = {}

@classmethod
async def build(
Expand Down Expand Up @@ -106,7 +98,7 @@ async def _build(
persist: bool = True,
) -> TableInstance:
model = table_class(_ignore_missing=True)
defaults = {} if not defaults else defaults
defaults = defaults or {}

for column, value in defaults.items():
if isinstance(column, str):
Expand Down Expand Up @@ -159,29 +151,110 @@ def _randomize_attribute(cls, column: Column) -> t.Any:
Column class to randomize.

"""
random_value: t.Any
if column.value_type == Decimal:
precision, scale = column._meta.params["digits"] or (4, 2)
random_value = RandomBuilder.next_float(
maximum=10 ** (precision - scale), scale=scale
)
elif column.value_type == datetime.datetime:
tz_aware = getattr(column, "tz_aware", False)
random_value = RandomBuilder.next_datetime(tz_aware=tz_aware)
elif column.value_type == list:
length = RandomBuilder.next_int(maximum=10)
base_type = t.cast(Array, column).base_column.value_type
random_value = [
cls.__DEFAULT_MAPPER[base_type]() for _ in range(length)
]
elif column._meta.choices:
reg = cls.get_registry(column)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can probably go into the else block, as we don't use it if the column has choices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid observation. It seems that the presence of reg is a result of the requirements in the previous version, where it was needed for multiple elif-else blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is a good idea.

    @classmethod
    def _randomize_attribute(cls, column: Column) -> t.Any:
        reg = cls.get_registry(column)
        random_value = reg.get(enum.Enum, reg[column.value_type])()
        if isinstance(column, (JSON, JSONB)):
            return json.dumps({"value": random_value})
        return random_value


    @classmethod
    def _get_local_mapper(cls, column: Column) -> t.Dict[t.Type, t.Callable]:
	...
        if _choices := column._meta.choices:
            local_mapper[enum.Enum] = partial(
                RandomBuilder.next_enum, _choices)

        return local_mapper

if column._meta.choices:
random_value = RandomBuilder.next_enum(column._meta.choices)
else:
random_value = cls.__DEFAULT_MAPPER[column.value_type]()
random_value = reg[column.value_type]()

if "length" in column._meta.params and isinstance(random_value, str):
return random_value[: column._meta.params["length"]]
elif isinstance(column, (JSON, JSONB)):
if isinstance(column, (JSON, JSONB)):
return json.dumps({"value": random_value})

return random_value

@classmethod
def get_registry(
cls, column: Column
) -> MappingProxyType[t.Type, t.Callable]:
"""
This serves as the public API allowing users to **view**
the complete registry for the specified column.

:param column:
Column class to randomize.

"""
default_mapper = cls.__DEFAULT_MAPPER
if not default_mapper: # execute once only
for typ, callable_ in RandomBuilder.get_mapper().items():
default_mapper[typ] = callable_

# order matters
reg = {
**default_mapper,
**cls._get_local_mapper(column),
**cls._get_other_mapper(column),
}

if column.value_type == list:
reg[list] = partial(
RandomBuilder.next_list,
reg[t.cast(Array, column).base_column.value_type],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to make things too insane, but multidimensional arrays are possible:

Array(Array(Integer())

I wonder what would happen here in that situation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for bringing this situation to my attention. I was curious about the behavior in our current codebase, so I conducted a quick test. It seems that the current implementation will throw a KeyError for this situation (please correct me if I'm mistaken).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit off-topic, but I wanted to try out this behavior in the Piccolo playground, and it doesn't seem to be working. The code works fine if I just launch a terminal and enter the shell.

piccolo playground run --engine=sqlite3
In [1]: from piccolo.table import Table

In [2]: from piccolo.columns import Array, BigInt

In [3]: class MyTable(Table):
   ...:     my_column = Array(Array(BigInt()))
   ...:
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 class MyTable(Table):
      2     my_column = Array(Array(BigInt()))

Cell In[3], line 2, in MyTable()
      1 class MyTable(Table):
----> 2     my_column = Array(Array(BigInt()))

NameError: name 'Array' is not defined

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrycw This happens because the Array and BigInt columns are not imported into the Playground application. If you patch the local Piccolo installation and add these two columns, everything works fine. Maybe should add all the possible columns to import and that would solve the problem..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sinisaos Thank you very much for identifying the source of the issue and providing the solution. It's working now.

Copy link
Member

@dantownsend dantownsend Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the current implementation of ModelBuilder doesn't handle multidimensional arrays, so I wouldn't worry about it if it's a tricky fix. Array columns now have two methods to help with this kind of thing: _get_dimensions and _get_inner_value_type.

)
return MappingProxyType(reg)

@classmethod
def _get_local_mapper(cls, column: Column) -> t.Dict[t.Type, t.Callable]:
"""
This classmethod encapsulates the desired logic, utilizing information
from the column.

:param column:
Column class to randomize.
"""
local_mapper: t.Dict[t.Type, t.Callable] = {}

precision, scale = column._meta.params.get("digits") or (4, 2)
local_mapper[Decimal] = partial(
RandomBuilder.next_decimal, precision, scale
)
Comment on lines +206 to +209
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If RandomBuilder.next_decimal incorporates default values for precision and scale (like this PR), the logic can be simplified to:

        if precision_scale := column._meta.params.get("digits"):
            local_mapper[Decimal] = partial(
                RandomBuilder.next_decimal, *precision_scale
            )


tz_aware = getattr(column, "tz_aware", False)
local_mapper[datetime.datetime] = partial(
RandomBuilder.next_datetime, tz_aware
)

if _length := column._meta.params.get("length"):
local_mapper[str] = partial(RandomBuilder.next_str, _length)

return local_mapper

@classmethod
def _get_other_mapper(cls, column: Column) -> t.Dict[t.Type, t.Callable]:
"""
This is a hook that allows users to register their own random type
callable. If the callable has a parameter named `column`, we assist
by injecting `column` using `partial`.

:param column:
Column class to randomize.

Examples::

# a callable not utilizing column information
ModelBuilder.register_random_type(str, lambda: "piccolo")

# a callable utilizing the column information
def next_str(column: Column) -> str:
length = column._meta.params.get("length", 5)
return "".join("a" for _ in range(length))
)
ModelBuilder.register_random_type(str, next_str)

"""
other_mapper: t.Dict[t.Type, t.Callable] = {}
for typ, callable_ in cls.__OTHER_MAPPER.items():
sig = inspect.signature(callable_)
if sig.parameters.get("column"):
other_mapper[typ] = partial(callable_, column)
else:
other_mapper[typ] = callable_
return other_mapper

@classmethod
def register_type(cls, typ: t.Type, callable_: t.Callable) -> None:
cls.__OTHER_MAPPER[typ] = callable_

@classmethod
def unregister_type(cls, typ: t.Type) -> None:
Comment on lines +253 to +258
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being able to register custom types is cool, but I wonder about the main use cases.

You can specify defaults at the moment:

await ModeBuilder.build(MyTable, defaults={MyTable.some_column: "foo"})

I'm not against being able to override how types are handled, but we just need to articulate to users when it's appropriate vs using defaults.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for bringing up this question, it prompted me to reflect on the code and its implications.

The main difference between using default and register_type lies in their respective purposes:

  • Utilizing default is suitable when users generally find our provided random logic satisfactory, but they require a hardcoded value for a specific column on a one-time basis.
  • On the other hand, employing register_type is appropriate when users desire a custom implementation for a type, effectively overwriting our default random logic for that specific type.
    Here are three distinct use cases:
    • (1) Types not provided by us: For instance, the type like next_decimal is not available in the current release version, but users can implement their own logic for the type and inject it into the ModelBuilder.
    • (2) User preference for specific logic: In scenarios where we introduce new features, such as the shiny next_decimal logic (returning decimal.Decimal if column.value_type is decimal.Decimal), users may prefer the previous implementation or have specific requirements. With register_type, they have the flexibility to override the default behavior.
    • (3) Unanticipated user cases: This aspect is particularly valuable for registration. For example, consider a user who initially builds a successful e-commerce platform in the UK using Piccolo. Later, they expand into Asia and encounter legal requirements necessitating the storage of customer names in local languages. If ModelBuilder does not support non-English characters, users can register their own implementations to address this issue.

A draft test for situations (1) and (2) might look like this:

class TableWithDecimal(Table):
    numeric = Numeric()
    numeric_with_digits = Numeric(digits=(4, 2))
    decimal = Decimal()
    decimal_with_digits = Decimal(digits=(4, 2))

class TestModelBuilder(unittest.TestCase):
    ...
    def test_registry_overwritten1(self):
        table = ModelBuilder.build_sync(TableWithDecimal)
        for key, value in table.to_dict().items():
            if key != "id":
                self.assertIsInstance(value, decimal.Decimal)

        def fake_next_decimal(column: Column) -> float:
            """will return `float` instead of `decimal.Decimal`"""
            precision, scale = column._meta.params["digits"] or (4, 2)
            return RandomBuilder.next_float(
                maximum=10 ** (precision - scale), scale=scale
            )

        ModelBuilder.register_type(decimal.Decimal, fake_next_decimal)
        overwritten_table = ModelBuilder.build_sync(TableWithDecimal)
        for key, value in overwritten_table.to_dict().items():
            if key != "id":
                self.assertIsInstance(value, float)

A draft test for situations (3) might look like this:

class TestModelBuilder(unittest.TestCase):
    ...
    def test_registry_overwritten2(self):
        choices = "一二三"  # Chinese characters

        def next_str(length: int = 3) -> str:
            # Chinese names often consist of three Chinese characters
            return "".join(random.choice(choices) for _ in range(length))

        ModelBuilder.register_type(str, next_str)
        manager1 = ModelBuilder.build_sync(Manager)
        self.assertTrue(all(char_ in choices for char_ in manager1.name))

        poster1 = ModelBuilder.build_sync(Poster)
        self.assertTrue(all(char_ in choices for char_ in poster1.content))

        ModelBuilder.unregister_type(str)
        manager2 = ModelBuilder.build_sync(Manager)
        self.assertTrue(all(char_ not in choices for char_ in manager2.name))

        poster2 = ModelBuilder.build_sync(Poster)
        self.assertTrue(all(char_ not in choices for char_ in poster2.content))

The scenario is as follows: Manager1 is a locally hired individual, while Manager2 is dispatched from the UK. Both are working on the poster using their native languages.

Finally, I realized I had overlooked the magic behavior of Python's name mangling rules. For instance:

>>> class ModelBuilder:
...     __OTHER_MAPPER = {}
...
>>> ModelBuilder.__OTHER_MAPPER
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'ModelBuilder' has no attribute '__OTHER_MAPPER'
>>> ModelBuilder._ModelBuilder__OTHER_MAPPER
{}

As a result, the previous test code might be a bit off. I need to use the following code for the setup and teardown phases for each test:

    def setUp(self) -> None:
        ModelBuilder._ModelBuilder__OTHER_MAPPER.clear()  # type: ignore

    def tearDown(self) -> None:
        ModelBuilder._ModelBuilder__OTHER_MAPPER.clear()  # type: ignore

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining the rationale behind it - it makes sense.

If I was to completely redesign ModelBuilder, I probably wouldn't have class methods. Instead of:

await ModelBuilder.build(MyTable)

I would have:

await ModelBuilder(some_option=True).build(MyTable)

So we can configure ModelBuilder's behaviour easier. For registering types we could have:

custom_builder = ModelBuilder(types={...})

await custom_builder.build(MyTable)

We could allow the types to be passed in via the build method instead:

await ModelBuilder.build(MyTable, types={...})

If register and unregister work globally, there are pros and cons. The main pro is you only need to set it up once (e.g. in a session fixture of Pytest). But if you were to somehow run your tests in parallel, it might be problematic.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for sharing your thoughts with me. Personally, I am inclined towards the instance method approach. However, implementing this change might break the current interface. I propose a three-stage transition plan:

  1. First stage: Utilize the concept of descriptors to distinguish between calls from the class or instance. Initially, we move the current implementation to the class branch to maintain user experience. Simultaneously, we start implementing the new concept in the instance branch, issuing an experimental warning.

  2. Second stage: If the new concept gains appreciation from users or developers, we add a deprecated warning to the class branch.

  3. Third stage: Remove the class branch and clean up the code to ensure all methods are instance methods by the end.

During the first two stages, we'll keep the class branch unchanged and encourage users to try out the new syntax and the new features. If we reach the third stage, users who prefer the class branch might need to adjust their habits from using await ModelBuilder.build(...) to await ModelBuilder().build() or ModelBuilder.build_sync(...) to ModelBuilder().build_sync(...).

The concept of descriptors is relatively straightforward, but it can sometimes feel too magical to grasp. I often need a refresher before coding if I haven't touched it for a long time. Fortunately, we don't need the complex __get__ and __set__ logic for the data descriptor. A simple non-data descriptor should suffice for our use case. With the help of this post, I've drafted a concept code as follows:

import asyncio
import inspect
import typing as t
from concurrent.futures import ThreadPoolExecutor


def run_sync(coroutine: t.Coroutine):
    try:
        # We try this first, as in most situations this will work.
        return asyncio.run(coroutine)
    except RuntimeError:
        # An event loop already exists.
        with ThreadPoolExecutor(max_workers=1) as executor:
            future = executor.submit(asyncio.run, coroutine)
            return future.result()


class dichotomy:
    def __init__(self, f):
        self.f = f

    def __get__(self, instance, owner):
        cls_or_inst = instance if instance is not None else owner
        if inspect.iscoroutine(self.f):
            async def newfunc(*args, **kwargs):
                return await self.f(cls_or_inst, *args, **kwargs)
        else:
            def newfunc(*args, **kwargs):
                return self.f(cls_or_inst, *args, **kwargs)
        return newfunc


class ModelBuilder:
    def __init__(self, *args, **kwargs):
        self._types = "..."  # Some information for instance method

    @dichotomy
    async def build(self_or_cls, *args, **kwargs):
        if inspect.isclass(self_or_cls):
            print("called as a class method from build")
            cls = self_or_cls
            await cls._build()
        else:
            print("called as an instance method from build")
            self = self_or_cls
            await self._build()

    @dichotomy
    def build_sync(self_or_cls, *args, **kwargs):
        return run_sync(self_or_cls.build())

    @dichotomy
    async def _build(self_or_cls, *args, **kwargs):
        if inspect.isclass(self_or_cls):
            print("called as a class method from _build", end="\n"*2)
            cls = self_or_cls  # noqa: F841
            # Current implementation remains here.
        else:
            print("called as an instance method from _build")
            self = self_or_cls
            # Some information can be retrieved.
            print(f'{self._types=}', end="\n"*2)
            # Our new logics


async def main():
    print('Async ModelBuilder.build: ')
    await ModelBuilder.build()

    print('Async ModelBuilder().build: ')
    await ModelBuilder().build()

    print('Sync ModelBuilder.build: ')
    ModelBuilder.build_sync()

    print('Sync ModelBuilder().build: ')
    ModelBuilder().build_sync()

if __name__ == '__main__':
    asyncio.run(main())
Async ModelBuilder.build: 
called as a class method from build
called as a class method from _build

Async ModelBuilder().build: 
called as an instance method from build
called as an instance method from _build
self._types='...'

Sync ModelBuilder.build: 
called as a class method from build
called as a class method from _build

Sync ModelBuilder().build: 
called as an instance method from build
called as an instance method from _build
self._types='...'

Finally, I agree that making register and unregister work globally could make it challenging to verify test results in parallel scenarios. I might lean towards using instance methods for the registering issue again.

These are just rough ideas that came to mind. I'm open to further discussions and refinements.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using descriptors is an interesting idea. I've used them sparingly before - as you say, they're very powerful, but can be confusing.

There's a lot of really good ideas in this PR, and I don't want to bog things down. I wonder if we could add this in a subsequent PR.

Copy link
Contributor Author

@jrycw jrycw Mar 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly! Here are some options to consider for closing this PR:

  • Closing the PR without merging any changes.
  • Keeping the current code as is, while implementing the next_decimal functionality and updating related code.
  • Utilizing the latest commit of this PR while removing the option for users to register custom types.
  • Merging the PR with its latest commit.
  • Considering any other suggestions or alternatives.

I'm open to any of these choices. @dantownsend , what are your thoughts on this matter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrycw Sorry for the delay on this - I haven't forgotten about it, I'm just trying to finish off a couple of PRs.

I'm interested to know your thoughts on this:

#978

If you think it's a good idea or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dantownsend no worries at all. It's great to see the project progressing on various fronts. I'll make an effort to review it and share any opinions or feedback I may have.

if typ in cls.__OTHER_MAPPER:
del cls.__OTHER_MAPPER[typ]
38 changes: 37 additions & 1 deletion piccolo/testing/random_builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import decimal
import enum
import random
import string
Expand All @@ -7,6 +8,27 @@


class RandomBuilder:
@classmethod
def get_mapper(cls) -> t.Dict[t.Type, t.Callable]:
"""
This is the public API for users to get the
provided random mapper.

"""
return {
bool: cls.next_bool,
bytes: cls.next_bytes,
datetime.date: cls.next_date,
datetime.datetime: cls.next_datetime,
float: cls.next_float,
decimal.Decimal: cls.next_decimal,
int: cls.next_int,
str: cls.next_str,
datetime.time: cls.next_time,
datetime.timedelta: cls.next_timedelta,
uuid.UUID: cls.next_uuid,
}

@classmethod
def next_bool(cls) -> bool:
return random.choice([True, False])
Expand Down Expand Up @@ -43,12 +65,21 @@ def next_enum(cls, e: t.Type[enum.Enum]) -> t.Any:
def next_float(cls, minimum=0, maximum=2147483647, scale=5) -> float:
return round(random.uniform(minimum, maximum), scale)

@classmethod
def next_decimal(
cls, precision: int = 4, scale: int = 2
) -> decimal.Decimal:
float_number = cls.next_float(
maximum=10 ** (precision - scale), scale=scale
)
return decimal.Decimal(str(float_number))

@classmethod
def next_int(cls, minimum=0, maximum=2147483647) -> int:
return random.randint(minimum, maximum)

@classmethod
def next_str(cls, length=16) -> str:
def next_str(cls, length: int = 16) -> str:
return "".join(
random.choice(string.ascii_letters) for _ in range(length)
)
Expand All @@ -72,3 +103,8 @@ def next_timedelta(cls) -> datetime.timedelta:
@classmethod
def next_uuid(cls) -> uuid.UUID:
return uuid.uuid4()

@classmethod
def next_list(cls, callable_: t.Callable) -> t.List[t.Any]:
length = cls.next_int(maximum=10)
return [callable_() for _ in range(length)]
66 changes: 66 additions & 0 deletions tests/testing/test_model_builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import asyncio
import builtins
import json
import random
import typing as t
import unittest

from piccolo.columns import (
Array,
Column,
Decimal,
ForeignKey,
Integer,
Expand Down Expand Up @@ -223,3 +226,66 @@ def test_json(self):
.run_sync()
):
self.assertIsInstance(facilities, dict)


@engines_skip("cockroach")
class TestModelBuilder2(unittest.TestCase):
@classmethod
def setUpClass(cls):
create_db_tables_sync(*TABLES)

@classmethod
def tearDownClass(cls) -> None:
drop_db_tables_sync(*TABLES)

def setUp(self) -> None:
ModelBuilder.__OTHER_MAPPER = {}

def tearDown(self) -> None:
ModelBuilder.__OTHER_MAPPER = {}

def test_register(self):
ModelBuilder.register_type(str, lambda: "piccolo")
manager = ModelBuilder.build_sync(Manager)
self.assertEqual(manager.name, "piccolo")

def test_register_with_column_info(self):
def next_str(column: Column) -> str:
length = column._meta.params.get("length", 5)
return "".join("a" for _ in range(length))

ModelBuilder.register_type(str, next_str)
manager = ModelBuilder.build_sync(Manager)
self.assertEqual(len(manager.name), 50)

post = ModelBuilder.build_sync(Poster)
self.assertEqual(len(post.content), 5)

def test_register_same_type(self):
ModelBuilder.register_type(str, lambda: "piccolo")
ModelBuilder.register_type(str, lambda: "PICCOLO")
manager = ModelBuilder.build_sync(Manager)
self.assertEqual(manager.name, "PICCOLO")

def test_unregister(self):
ModelBuilder.register_type(str, lambda: "piccolo")
ModelBuilder.unregister_type(str)
manager = ModelBuilder.build_sync(Manager)
self.assertNotEqual(manager.name, "piccolo")

def test_unregister_same_type(self):
ModelBuilder.unregister_type(str)
ModelBuilder.unregister_type(str)

def test_unregister_any_type(self):
ModelBuilder.unregister_type(random.choice(dir(builtins)))

def test_get_registry(self):
def next_str() -> str:
return "piccolo"

ModelBuilder.register_type(str, next_str)
reg = ModelBuilder.get_registry(Varchar())

self.assertIn(str, reg)
self.assertIn(next_str, reg.values())
10 changes: 10 additions & 0 deletions tests/testing/test_random_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def test_next_float(self):
random_float = RandomBuilder.next_float(maximum=1000)
self.assertLessEqual(random_float, 1000)

def test_next_decimal(self):
random_decimal = RandomBuilder.next_decimal(5, 2)
self.assertLessEqual(random_decimal, 1000)

def test_next_int(self):
random_int = RandomBuilder.next_int()
self.assertLessEqual(random_int, 2147483647)
Expand All @@ -52,3 +56,9 @@ def test_next_timedelta(self):

def test_next_uuid(self):
RandomBuilder.next_uuid()

def test_next_list(self):
for typ, callable_ in RandomBuilder.get_mapper().items():
random_list = RandomBuilder.next_list(callable_)
self.assertIsInstance(random_list, list)
self.assertTrue(all(isinstance(elem, typ) for elem in random_list))