Skip to content

christianhelle/autofaker

Repository files navigation

build build build

Quality Gate Status codecov PyPI buymeacoffee

AutoFaker

AutoFaker is a Python library designed to minimize the setup/arrange phase of your unit tests by removing the need to manually write code to create anonymous variables as part of a test cases setup/arrange phase.

This library is heavily inspired by AutoFixture and was initially created for simplifying how to write unit tests for ETL (Extract-Transform-Load) code running from a python library on an Apache Spark cluster in Big Data solutions.

When writing unit tests you normally start with creating objects that represent the initial state of the test. This phase is called the arrange or setup phase of the test. In most cases, the system you want to test will force you to specify much more information than you really care about, so you frequently end up creating objects with no influence on the test itself just simply to satisfy the compiler/interpreter

AutoFaker is available from PyPI and should be installed using pip

pip install autofaker

AutoFaker can help by creating such anonymous variables for you. Here's a simple example:

import unittest
from autofaker import Autodata

class Calculator:
  def add(self, number1: int, number2: int):
    return number1 + number2

class CalculatorTests(unittest.TestCase):
    def test_can_add_two_numbers(self):      
        # arrange
        numbers = Autodata.create_many(int, 2)
        sut = Autodata.create(Calculator)        
        # act
        result = sut.add(numbers[0], numbers[1])        
        # assert
        self.assertEqual(numbers[0] + numbers[1], result)

Since the point of this library is to simplify the arrange step of writing unit tests, we can use the @autodata and @fakedata are available to explicitly state whether to use anonymous variables or fake data and construct our system under test. To use this you can either define the types or the arguments as function arguments to the decorator, or specify argument annotations

import unittest
from autofaker import autodata

class Calculator:
  def add(self, number1: int, number2: int):
    return number1 + number2

class CalculatorTests(unittest.TestCase):
    @autodata(Calculator, int, int)
    def test_can_add_two_numbers_using_test_arguments(self, sut, number1, number2):
        result = sut.add(number1, number2)
        self.assertEqual(number1 + number2, result)

    @autodata()
    def test_can_add_two_numbers_using_annotated_arguments(self, 
                                                           sut: Calculator, 
                                                           number1: int, 
                                                           number2: int):
        result = sut.add(number1, number2)
        self.assertEqual(number1 + number2, result)

There are times when completely anonymous variables don't make much sense, especially in data centric scenarios. For these use cases this library uses Faker for generating fake data. This option is enabled by setting use_fake_data to True when calling the Autodata.create() function

from dataclasses import dataclass
from autofaker import Autodata

@dataclass
class DataClass:
    id: int
    first_name: str
    last_name: str
    job: str

data = Autodata.create(DataClass, use_fake_data=True)

print(f'id:     {data.id}')
print(f'name:   {data.first_name} {data.last_name}')
print(f'job:    {data.job}\n')

The following code above might output something like:

id:     8952
name:   Justin Wise
job:    Chief Operating Officer

Supported OS and Python versions

Windows MacOS Linux

Supported data types

Currently autofaker supports creating anonymous variables for the following data types:

Built-in types:

  • int
  • float
  • str
  • complex
  • range
  • bytes
  • bytearray
  • tuple
  • set
  • frozenset
  • dict
  • decimal.Decimal
  • uuid.UUID
  • pathlib.Path

Datetime types:

  • datetime
  • date
  • time
  • timedelta

Typing generics:

  • typing.Tuple[T, ...]
  • typing.Set[T]
  • typing.FrozenSet[T]
  • typing.Dict[K, V]
  • typing.Optional[T]
  • typing.List[T]

Classes:

  • Simple classes
  • @dataclass
  • Nested classes (and recursion)
  • Classes containing lists of other types
  • Enum classes
  • typing.Literal types

Dataframes:

  • Pandas dataframe

Example usages

Create anonymous built-in types like int, float, str and datetime types like datetime and date

print(f'anonymous string:    {Autodata.create(str)}')
print(f'anonymous int:       {Autodata.create(int)}')
print(f'anonymous float:     {Autodata.create(float)}')
print(f'anonymous complex:   {Autodata.create(complex)}')
print(f'anonymous range:     {Autodata.create(range)}')
print(f'anonymous bytes:     {Autodata.create(bytes)}')
print(f'anonymous bytearray: {Autodata.create(bytearray)}')
print(f'anonymous datetime:  {Autodata.create(datetime)}')
print(f'anonymous date:      {Autodata.create(datetime.date)}')

The code above might output the following

anonymous string:    f91954f1-96df-463f-a427-665c99213395
anonymous int:       2066712686
anonymous float:     725758222.8712853
anonymous datetime:  2017-06-19 02:40:41.000084
anonymous date:      2019-11-10 00:00:00

Create anonymous instances of additional built-in and standard library types

import decimal
import uuid
import pathlib
from datetime import time, timedelta

print(f'anonymous tuple:     {Autodata.create(tuple)}')
print(f'anonymous set:       {Autodata.create(set)}')
print(f'anonymous frozenset: {Autodata.create(frozenset)}')
print(f'anonymous dict:      {Autodata.create(dict)}')
print(f'anonymous Decimal:   {Autodata.create(decimal.Decimal)}')
print(f'anonymous UUID:      {Autodata.create(uuid.UUID)}')
print(f'anonymous Path:      {Autodata.create(pathlib.Path)}')
print(f'anonymous time:      {Autodata.create(time)}')
print(f'anonymous timedelta: {Autodata.create(timedelta)}')

The code above might output the following

anonymous tuple:     (9655, '1608f20f-c563-41ff-b3df-2be21be71437', 2309.52)
anonymous set:       {'eff62089-1834-4b5c-854e-68ac8f28979f', ...}
anonymous frozenset: frozenset({'0ef46f14-70ce-4d0b-a099-625d0f881602', ...})
anonymous dict:      {'fd1a7430-5b5f-4dc0-974d-65184a406d25': 3431, ...}
anonymous Decimal:   2508.6919872240996
anonymous UUID:      751de33e-401e-42f2-acdb-3df9d1f9eb62
anonymous Path:      ee388b30-eb11-46af-b2f0/e1ab4d2d-7a6f-4ee9
anonymous time:      23:09:39.638523
anonymous timedelta: 165 days, 1:19:58.470884

Create anonymous instances of typed generics

from typing import Tuple, Set, FrozenSet, Dict, Optional

print(f'Tuple[int, str]:  {Autodata.create(Tuple[int, str])}')
print(f'Set[int]:         {Autodata.create(Set[int])}')
print(f'FrozenSet[str]:   {Autodata.create(FrozenSet[str])}')
print(f'Dict[str, int]:   {Autodata.create(Dict[str, int])}')
print(f'Optional[int]:    {Autodata.create(Optional[int])}')

The code above might output the following

Tuple[int, str]:  (7032, '351236d0-378e-45f6-bf58-77e2ac2bfc48')
Set[int]:         {1408, 8224, 5107}
FrozenSet[str]:   frozenset({'5f5eaf3c-1926-4d34-ae32-ef5a3a549c5f', ...})
Dict[str, int]:   {'65174a9b-76f3-4d1c-a1a4': 3817, ...}
Optional[int]:    8051

Creates an anonymous class

class SimpleClass:
    id = -1
    text = 'test'

cls = Autodata.create(SimpleClass)
print(f'id = {cls.id}')
print(f'text = {cls.text}')

The code above might output the following

id = 2020177162
text = ac54a65d-b4a3-4eda-a840-eb948ad10d5f

Create a collection of an anonymous class

class SimpleClass:
    id = -1
    text = 'test'

classes = Autodata.create_many(SimpleClass)
for cls in classes:
  print(f'id = {cls.id}')
  print(f'text = {cls.text}')
  print()

The code above might output the following

id = 242996515
text = 5bb60504-ccca-4104-9b7f-b978e52a6518

id = 836984239
text = 079df61e-a87e-4f26-8196-3f44157aabd6

id = 570703150
text = a3b86f08-c73a-4730-bde7-4bdff5360ef4

Creates an anonymous dataclass

from dataclasses import dataclass

@dataclass
class DataClass:
    id: int
    text: str

cls = Autodata.create(DataClass)
print(f'id = {cls.id}')
print(f'text = {cls.text}')

The code above might output the following

id = 314075507
text = 4a3b3cae-f4cf-4502-a7f3-61115a1e0d2a

Creates an anonymous dataclass using fake data

@dataclass
class DataClass:
    id: int

    name: str
    address: str
    job: str

    country: str
    currency_name: str
    currency_code: str

    email: str
    safe_email: str
    company_email: str

    hostname: str
    ipv4: str
    ipv6: str

    text: str


data = Autodata.create(DataClass, use_fake_data=True)

print(f'id:               {data.id}')
print(f'name:             {data.name}')
print(f'job:              {data.job}\n')
print(f'address:\n{data.address}\n')

print(f'country:          {data.country}')
print(f'currency name:    {data.currency_name}')
print(f'currency code:    {data.currency_code}\n')

print(f'email:            {data.email}')
print(f'safe email:       {data.safe_email}')
print(f'work email:       {data.company_email}\n')

print(f'hostname:         {data.hostname}')
print(f'IPv4:             {data.ipv4}')
print(f'IPv6:             {data.ipv6}\n')

print(f'text:\n{data.text}')

The code above might output the following

id:               8952
name:             Justin Wise
job:              Chief Operating Officer

address:
65939 Hernandez Parks
Rochaport, NC 41760

country:          Equatorial Guinea
currency name:    Burmese kyat
currency code:    ERN

email:            smithjohn@example.com
safe email:       kent11@example.com
work email:       marissagreen@brown-cole.com

hostname:         db-90.hendricks-west.org
IPv4:             66.139.143.242
IPv6:             895d:82f7:7c13:e7cb:f35d:c93:aeb2:8eeb

text:
Movie author culture represent. Enjoy myself over physical green lead but home.
Share wind factor far minute produce significant. Sense might fact leader.

Create an anonymous class with nested types

class NestedClass:
    id = -1
    text = 'test'
    inner = SimpleClass()

cls = Autodata.create(NestedClass)
print(f'id = {cls.id}')
print(f'text = {cls.text}')
print(f'inner.id = {cls.inner.id}')
print(f'inner.text = {cls.inner.text}')

The code above might output the following

id = 1565737216
text = e66ecd5c-c17a-4426-b755-36dfd2082672
inner.id = 390282329
inner.text = eef94b5c-aa95-427a-a9e6-d99e2cc1ffb2

Create a collection of an anonymous class with nested types

class NestedClass:
    id = -1
    text = 'test'
    inner = SimpleClass()

classes = Autodata.create_many(NestedClass)
for cls in classes:
  print(f'id = {cls.id}')
  print(f'text = {cls.text}')
  print(f'inner.id = {cls.inner.id}')
  print(f'inner.text = {cls.inner.text}')
  print()

The code above might output the following

id = 1116454042
text = ceeecf0c-7375-4f3a-8d4b-6d7a4f2b20fd
inner.id = 1067027444
inner.text = 079573ce-1ef4-408d-8984-1dbc7b0d0b80

id = 730390288
text = ff3ca474-a69d-4ff6-95b4-fbdb1bea7cdb
inner.id = 1632771208
inner.text = 9423e824-dc8f-4145-ba47-7301351a91f8

id = 187364960
text = b31ca191-5031-43a2-870a-7bc7c99e4110
inner.id = 1705149100
inner.text = e703a117-ba4f-4201-a31b-10ab8e54a673

Create a dataclass using additional built-in types and typing generics

import decimal
import uuid
import pathlib
from datetime import time, timedelta
from typing import Set, Dict, Tuple, Optional
from dataclasses import dataclass
from autofaker import Autodata

@dataclass
class AdvancedDataClass:
    id: int
    name: str
    amount: decimal.Decimal
    uid: uuid.UUID
    file_path: pathlib.Path
    created_time: time
    duration: timedelta
    tags: Set[str]
    scores: Tuple[int, str]
    metadata: Dict[str, int]
    optional_name: Optional[str]

data = Autodata.create(AdvancedDataClass)

print(f'id:           {data.id}')
print(f'name:         {data.name}')
print(f'amount:       {data.amount}')
print(f'uid:          {data.uid}')
print(f'file_path:    {data.file_path}')
print(f'created_time: {data.created_time}')
print(f'duration:     {data.duration}')
print(f'tags:         {data.tags}')
print(f'scores:       {data.scores}')
print(f'metadata:     {data.metadata}')
print(f'optional:     {data.optional_name}')

The code above might output the following

id:           2225
name:         28a78977-8218-471a-aa3b-5172bd267e9a
amount:       8071.9111587519055
uid:          4bfc05f9-ad4e-4906-96a0-74f4ef60974f
file_path:    0da7ee48-1008-4e72/ffc6c087-b690-44d3
created_time: 15:25:29.820442
duration:     177 days, 23:33:16.414807
tags:         {'191751c3-11a6-471b', '63508ce7-b14a-433f', ...}
scores:       (4467, 'a1255ae9-b23c-4cfa-8861-213e12b41030')
metadata:     {'193b3ddc-3172-49ef': 1744, ...}
optional:     983e4c69-0426-4488-ba98-559e77075c03

Create a Pandas DataFrame using anonymous data generated from a specified type

class DataClass:
    id = -1
    type = '' 
    value = 0

pdf = Autodata.create_pandas_dataframe(DataClass)
print(pdf)

The code above might output the following

          id                                  type       value
0  778090854  13537c5a-62e7-488b-836e-a4b17f2f3ae9  1049015695
1  602015506  c043ca8d-e280-466a-8bba-ec1e0539fe28  1016359353
2  387753717  986b3b1c-abf4-4bc1-95cf-0e979390e4f3   766159839

Create a Pandas DataFrame using fake data generated from a specified type

class DataClass:
    id = -1
    first_name = '' 
    last_name = 0
    phone_number = ''

pdf = Autodata.create_pandas_dataframe(DataClass, use_fake_data=True)
print(pdf)

The code above might output the following

  first_name    id last_name          phone_number
0   Lawrence  7670   Jimenez  001-172-307-0561x471
1      Bryan  9084    Walker         (697)893-6767
2       Paul  9824    Thomas    960.555.3577x65487

Create anonymous variables from typing.Literal types

from typing import Literal
from autofaker import Autodata

# Define Literal types
status_literal = Literal["pending", "approved", "rejected"]
number_literal = Literal[1, 2, 3]

# Create anonymous variables from Literals
status = Autodata.create(status_literal)
number = Autodata.create(number_literal)

print(f'Random status: {status}')
print(f'Random number: {number}')

The code above might output the following

Random status: approved
Random number: 2

Use typing.Literal in classes and dataclasses

from typing import Literal
from dataclasses import dataclass
from autofaker import Autodata

# Define Literal types
status_literal = Literal["pending", "approved", "rejected"]
priority_literal = Literal[1, 2, 3]

@dataclass
class Task:
    id: int
    title: str
    status: status_literal
    priority: priority_literal

# Create an anonymous Task with random values
task = Autodata.create(Task)

print(f'Task ID: {task.id}')
print(f'Title: {task.title}')
print(f'Status: {task.status}')
print(f'Priority: {task.priority}')

The code above might output the following

Task ID: 1565737216
Title: e66ecd5c-c17a-4426-b755-36dfd2082672
Status: rejected
Priority: 1

For tips and tricks on software development, check out my blog

If you find this useful and feel a bit generous then feel free to buy me a coffee ☕

About

Python library designed to minimize the setup/arrange phase of your unit tests

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors