From 5d120a83767d1fb02dfec2d1d072256b9063af73 Mon Sep 17 00:00:00 2001
From: Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>
Date: Wed, 24 Nov 2021 08:46:12 +0100
Subject: [PATCH] add Factories and Seeders

---
 cli/Commands/DB/Seed.php                      |  49 ++
 cli/studip                                    |   3 +
 composer.json                                 |   3 +-
 composer.lock                                 |  67 ++
 .../Factories/BelongsToManyRelationship.php   |  61 ++
 .../Factories/BelongsToRelationship.php       |  76 +++
 lib/classes/Database/Factories/Factory.php    | 635 ++++++++++++++++++
 lib/classes/Database/Factories/HasFactory.php |  31 +
 .../Database/Factories/Relationship.php       |  54 ++
 lib/classes/Database/Factories/Sequence.php   |  53 ++
 lib/classes/Database/Seeder.php               |  25 +
 .../Database/Seeders/ActivitiesSeeder.php     |  73 ++
 lib/classes/Database/Seeders/CourseSeeder.php |  45 ++
 .../Database/Seeders/DatabaseSeeder.php       |  19 +
 .../Database/Seeders/ExampleSeeder.php        |  89 +++
 lib/models/SimpleORMap.class.php              |   5 +
 lib/models/factories/CourseFactory.php        |  51 ++
 .../factories/Courseware/BlockFactory.php     |  29 +
 .../factories/Courseware/ContainerFactory.php |  41 ++
 .../Courseware/StructuralElementFactory.php   |  52 ++
 lib/models/factories/UserFactory.php          |  35 +
 tests/jsonapi/ModelFactoryTest.php            | 224 ++++++
 22 files changed, 1719 insertions(+), 1 deletion(-)
 create mode 100644 cli/Commands/DB/Seed.php
 create mode 100644 lib/classes/Database/Factories/BelongsToManyRelationship.php
 create mode 100644 lib/classes/Database/Factories/BelongsToRelationship.php
 create mode 100644 lib/classes/Database/Factories/Factory.php
 create mode 100644 lib/classes/Database/Factories/HasFactory.php
 create mode 100644 lib/classes/Database/Factories/Relationship.php
 create mode 100644 lib/classes/Database/Factories/Sequence.php
 create mode 100644 lib/classes/Database/Seeder.php
 create mode 100644 lib/classes/Database/Seeders/ActivitiesSeeder.php
 create mode 100644 lib/classes/Database/Seeders/CourseSeeder.php
 create mode 100644 lib/classes/Database/Seeders/DatabaseSeeder.php
 create mode 100644 lib/classes/Database/Seeders/ExampleSeeder.php
 create mode 100644 lib/models/factories/CourseFactory.php
 create mode 100644 lib/models/factories/Courseware/BlockFactory.php
 create mode 100644 lib/models/factories/Courseware/ContainerFactory.php
 create mode 100644 lib/models/factories/Courseware/StructuralElementFactory.php
 create mode 100644 lib/models/factories/UserFactory.php
 create mode 100644 tests/jsonapi/ModelFactoryTest.php

diff --git a/cli/Commands/DB/Seed.php b/cli/Commands/DB/Seed.php
new file mode 100644
index 00000000000..2c674edce01
--- /dev/null
+++ b/cli/Commands/DB/Seed.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Studip\Cli\Commands\DB;
+
+use Studip\Database\Seeders\DatabaseSeeder;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+class Seed extends Command
+{
+    protected static $defaultName = 'db:seed';
+
+    protected function configure(): void
+    {
+        $this->setDescription('Seed the database with records');
+        $this->addArgument('class', InputArgument::OPTIONAL, 'The class of the root seeder', DatabaseSeeder::class);
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+        $io->info('Migration starting at ' . date('d.m.Y H:i:s'));
+
+        $this->getSeeder($input)->__invoke();
+
+        $io->info('Database seeding completed successfully.');
+
+        return Command::SUCCESS;
+    }
+
+    /**
+     * Get a seeder instance from the container.
+     */
+    protected function getSeeder(InputInterface $input): \Studip\Database\Seeder
+    {
+        $class = $input->getArgument('class');
+
+        if (strpos($class, '\\') === false) {
+            $class = 'Studip\\Database\\Seeders\\' . $class;
+        }
+
+        $seeder = new $class();
+
+        return $seeder;
+    }
+}
diff --git a/cli/studip b/cli/studip
index c4addb93b93..bfb56999e64 100755
--- a/cli/studip
+++ b/cli/studip
@@ -31,6 +31,9 @@ $commands = [
     Commands\Cronjobs\CronjobWorker::class,
     Commands\Cronjobs\CronjobWorker::class,
     Commands\DB\Dump::class,
+    Commands\DB\MigrateEngine::class,
+    Commands\DB\MigrateFileFormat::class,
+    Commands\DB\Seed::class,
     Commands\Files\Dump::class,
     Commands\Fix\Biest7789::class,
     Commands\Fix\Biest7866::class,
diff --git a/composer.json b/composer.json
index b98b402c774..a6288229f42 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,8 @@
         "codeception/module-asserts": "^1.3",
         "overtrue/phplint": "^3.0",
         "phpstan/phpstan": "^1.8",
-        "symfony/var-dumper": "^5.4"
+        "symfony/var-dumper": "^5.4",
+        "fakerphp/faker": "^1.20"
     },
     "require": {
         "php": "^7.2",
diff --git a/composer.lock b/composer.lock
index dc1399eff55..d6b215ec8e5 100644
--- a/composer.lock
+++ b/composer.lock
@@ -5449,6 +5449,73 @@
             ],
             "time": "2022-12-30T00:15:36+00:00"
         },
+        {
+            "name": "fakerphp/faker",
+            "version": "v1.20.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/FakerPHP/Faker.git",
+                "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/37f751c67a5372d4e26353bd9384bc03744ec77b",
+                "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0",
+                "psr/container": "^1.0 || ^2.0",
+                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+            },
+            "conflict": {
+                "fzaninotto/faker": "*"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.4.1",
+                "doctrine/persistence": "^1.3 || ^2.0",
+                "ext-intl": "*",
+                "symfony/phpunit-bridge": "^4.4 || ^5.2"
+            },
+            "suggest": {
+                "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
+                "ext-curl": "Required by Faker\\Provider\\Image to download images.",
+                "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
+                "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
+                "ext-mbstring": "Required for multibyte Unicode string functionality."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "v1.20-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Faker\\": "src/Faker/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "François Zaninotto"
+                }
+            ],
+            "description": "Faker is a PHP library that generates fake data for you.",
+            "keywords": [
+                "data",
+                "faker",
+                "fixtures"
+            ],
+            "support": {
+                "issues": "https://github.com/FakerPHP/Faker/issues",
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.20.0"
+            },
+            "time": "2022-07-20T13:12:54+00:00"
+        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.11.1",
diff --git a/lib/classes/Database/Factories/BelongsToManyRelationship.php b/lib/classes/Database/Factories/BelongsToManyRelationship.php
new file mode 100644
index 00000000000..0c3fe0a3513
--- /dev/null
+++ b/lib/classes/Database/Factories/BelongsToManyRelationship.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+use SimpleORMap;
+
+class BelongsToManyRelationship
+{
+    /**
+     * The related factory instance.
+     *
+     * @var SimpleORMap
+     */
+    protected $factory;
+
+    /**
+     * The pivot attributes / attribute resolver.
+     *
+     * @var callable|array
+     */
+    protected $pivot;
+
+    /**
+     * The relationship name.
+     *
+     * @var string
+     */
+    protected $relationship;
+
+    /**
+     * Create a new attached relationship definition.
+     *
+     * @param  SimpleORMap  $factory
+     * @param  callable|array  $pivot
+     * @param  string  $relationship
+     * @return void
+     */
+    public function __construct($factory, $pivot, $relationship)
+    {
+        $this->factory = $factory;
+        $this->pivot = $pivot;
+        $this->relationship = $relationship;
+    }
+
+    /**
+     * Create the attached relationship for the given model.
+     *
+     * @param  SimpleORMap  $model
+     * @return void
+     */
+    public function createFor(SimpleORMap $model)
+    {
+        $collection = $this->factory instanceof Factory ? $this->factory->create([], $model) : $this->factory;
+
+        foreach ($collection as $attachable) {
+            $model
+                ->{$this->relationship}()
+                ->attach($attachable, is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot);
+        }
+    }
+}
diff --git a/lib/classes/Database/Factories/BelongsToRelationship.php b/lib/classes/Database/Factories/BelongsToRelationship.php
new file mode 100644
index 00000000000..7541791d15d
--- /dev/null
+++ b/lib/classes/Database/Factories/BelongsToRelationship.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+use SimpleORMap;
+
+class BelongsToRelationship
+{
+    /**
+     * The related factory instance.
+     *
+     * @var SimpleORMap
+     */
+    protected $factory;
+
+    /**
+     * The relationship name.
+     *
+     * @var string
+     */
+    protected $relationship;
+
+    /**
+     * The cached, resolved parent instance ID.
+     *
+     * @var mixed
+     */
+    protected $resolved;
+
+    /**
+     * Create a new "belongs to" relationship definition.
+     *
+     * @param  SimpleORMap  $factory
+     * @param  string  $relationship
+     * @return void
+     */
+    public function __construct($factory, $relationship)
+    {
+        $this->factory = $factory;
+        $this->relationship = $relationship;
+    }
+
+    /**
+     * Get the parent model attributes and resolvers for the given child model.
+     *
+     * @param  SimpleORMap  $model
+     * @return array
+     */
+    public function attributesFor(SimpleORMap $model)
+    {
+        $relationship = $model->{$this->relationship}();
+
+        return [
+            $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()),
+        ];
+    }
+
+    /**
+     * Get the deferred resolver for this relationship's parent ID.
+     *
+     * @param  string|null  $key
+     * @return \Closure
+     */
+    protected function resolver($key)
+    {
+        return function () use ($key) {
+            if (! $this->resolved) {
+                $instance = $this->factory instanceof Factory ? $this->factory->create() : $this->factory;
+
+                return $this->resolved = $key ? $instance->{$key} : $instance->getId();
+            }
+
+            return $this->resolved;
+        };
+    }
+}
diff --git a/lib/classes/Database/Factories/Factory.php b/lib/classes/Database/Factories/Factory.php
new file mode 100644
index 00000000000..5eab431e0f6
--- /dev/null
+++ b/lib/classes/Database/Factories/Factory.php
@@ -0,0 +1,635 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+use Closure;
+use Faker\Generator;
+use SimpleORMap;
+use Throwable;
+
+abstract class Factory
+{
+    /**
+     * The name of the factory's corresponding model.
+     *
+     * @var string|null
+     */
+    protected $model;
+
+    /**
+     * The number of models that should be generated.
+     *
+     * @var int|null
+     */
+    protected $count;
+
+    /**
+     * The state transformations that will be applied to the model.
+     *
+     * @var array
+     */
+    protected $states;
+
+    /**
+     * The parent relationships that will be applied to the model.
+     *
+     * @var array
+     */
+    protected $has;
+
+    /**
+     * The child relationships that will be applied to the model.
+     *
+     * @var array
+     */
+    protected $for;
+
+    /**
+     * The "after making" callbacks that will be applied to the model.
+     *
+     * @var array
+     */
+    protected $afterMaking;
+
+    /**
+     * The "after creating" callbacks that will be applied to the model.
+     *
+     * @var array
+     */
+    protected $afterCreating;
+
+    /**
+     * The current Faker instance.
+     *
+     * @var \Faker\Generator
+     */
+    protected $faker;
+
+    /**
+     * The default namespace where factories reside.
+     *
+     * @var string
+     */
+    protected static $namespace = 'Factories\\';
+
+    /**
+     * Create a new factory instance.
+     *
+     * @param  int|null  $count
+     * @param  array|null  $states
+     * @param  array|null  $has
+     * @param  array|null  $for
+     * @param  array|null  $afterMaking
+     * @param  array|null  $afterCreating
+     * @return void
+     */
+    public function __construct(
+        $count = null,
+        $states = null,
+        $has = null,
+        $for = null,
+        $afterMaking = null,
+        $afterCreating = null
+    ) {
+        $this->count = $count;
+        $this->states = $states ?: [];
+        $this->has = $has ?: [];
+        $this->for = $for ?: [];
+        $this->afterMaking = $afterMaking ?: [];
+        $this->afterCreating = $afterCreating ?: [];
+        $this->faker = $this->withFaker();
+    }
+
+    /**
+     * Define the model's default state.
+     *
+     * @return array
+     */
+    abstract public function definition();
+
+    /**
+     * Get a new factory instance for the given attributes.
+     *
+     * @param  callable|array  $attributes
+     * @return static
+     */
+    public static function new($attributes = [])
+    {
+        return (new static())->state($attributes)->configure();
+    }
+
+    /**
+     * Get a new factory instance for the given number of models.
+     *
+     * @param  int  $count
+     * @return static
+     */
+    public static function times(int $count)
+    {
+        return static::new()->count($count);
+    }
+
+    /**
+     * Configure the factory.
+     *
+     * @return $this
+     */
+    public function configure()
+    {
+        return $this;
+    }
+
+    /**
+     * Get the raw attributes generated by the factory.
+     *
+     * @param  array  $attributes
+     * @param  SimpleORMap|null  $parent
+     * @return array
+     */
+    public function raw($attributes = [], ?SimpleORMap $parent = null)
+    {
+        if ($this->count === null) {
+            return $this->state($attributes)->getExpandedAttributes($parent);
+        }
+
+        return array_map(function () use ($attributes, $parent) {
+            return $this->state($attributes)->getExpandedAttributes($parent);
+        }, range(1, $this->count));
+    }
+
+    /**
+     * Create a single model and persist it to the database.
+     *
+     * @param  array  $attributes
+     * @return SimpleORMap
+     */
+    public function createOne($attributes = [])
+    {
+        return $this->count(null)->create($attributes);
+    }
+
+    /**
+     * Create a collection of models and persist them to the database.
+     *
+     * @param  iterable  $records
+     * @return iterable
+     */
+    public function createMany(iterable $records)
+    {
+        $many = [];
+        foreach ($records as $record) {
+            $many[] = $this->state($record)->create();
+        }
+
+        return $many;
+    }
+
+    /**
+     * Create a collection of models and persist them to the database.
+     *
+     * @param  array  $attributes
+     * @param  SimpleORMap|null  $parent
+     * @return array|SimpleORMap
+     */
+    public function create($attributes = [], ?SimpleORMap $parent = null)
+    {
+        if (!empty($attributes)) {
+            return $this->state($attributes)->create([], $parent);
+        }
+
+        $results = $this->make($attributes, $parent);
+
+        if ($results instanceof SimpleORMap) {
+            $this->store([$results]);
+
+            $this->callAfterCreating([$results], $parent);
+        } else {
+            $this->store($results);
+
+            $this->callAfterCreating($results, $parent);
+        }
+
+        return $results;
+    }
+
+    /**
+     * Create a callback that persists a model in the database when invoked.
+     *
+     * @param  array  $attributes
+     * @param  SimpleORMap|null  $parent
+     * @return \Closure
+     */
+    public function lazy(array $attributes = [], ?SimpleORMap $parent = null)
+    {
+        return function () use ($attributes, $parent) {
+            return $this->create($attributes, $parent);
+        };
+    }
+
+    /**
+     * Store the results.
+     *
+     * @param  array  $results
+     * @return void
+     */
+    protected function store($results)
+    {
+        foreach ($results as $model) {
+            $model->store();
+            $this->createChildren($model);
+        }
+    }
+
+    /**
+     * Create the children for the given model.
+     *
+     * @param  SimpleORMap  $model
+     * @return void
+     */
+    protected function createChildren(SimpleORMap $model)
+    {
+        foreach ($this->has as $has) {
+            $has->createFor($model);
+        }
+    }
+
+    /**
+     * Make a single instance of the model.
+     *
+     * @param  callable|array  $attributes
+     * @return SimpleORMap
+     */
+    public function makeOne($attributes = [])
+    {
+        return $this->count(null)->make($attributes);
+    }
+
+    /**
+     * Create a collection of models.
+     *
+     * @param  array  $attributes
+     * @param  SimpleORMap|null  $parent
+     * @return array|SimpleORMap
+     */
+    public function make($attributes = [], ?SimpleORMap $parent = null)
+    {
+        if (!empty($attributes)) {
+            return $this->state($attributes)->make([], $parent);
+        }
+
+        if ($this->count === null) {
+            $instance = $this->makeInstance($parent);
+            $this->callAfterMaking([$instance]);
+
+            return $instance;
+        }
+
+        if ($this->count < 1) {
+            return [];
+        }
+
+        $instances = array_map(function () use ($parent) {
+            return $this->makeInstance($parent);
+        }, range(1, $this->count));
+
+        $this->callAfterMaking($instances);
+
+        return $instances;
+    }
+
+    /**
+     * Make an instance of the model with the given attributes.
+     *
+     * @param  SimpleORMap|null  $parent
+     * @return SimpleORMap
+     */
+    protected function makeInstance(?SimpleORMap $parent)
+    {
+        return $this->newModel($this->getExpandedAttributes($parent));
+    }
+
+    /**
+     * Get a raw attributes array for the model.
+     *
+     * @param  SimpleORMap|null  $parent
+     * @return mixed
+     */
+    protected function getExpandedAttributes(?SimpleORMap $parent)
+    {
+        return $this->expandAttributes($this->getRawAttributes($parent));
+    }
+
+    /**
+     * Get the raw attributes for the model as an array.
+     *
+     * @param  SimpleORMap|null  $parent
+     * @return array
+     */
+    protected function getRawAttributes(?SimpleORMap $parent)
+    {
+        $states =
+            count($this->for) === 0
+                ? $this->states
+                : array_merge(
+                    [
+                        function () {
+                            return $this->parentResolvers();
+                        },
+                    ],
+                    $this->states
+                );
+
+        $rawAttributes = array_reduce(
+            $states,
+            function ($carry, $state) use ($parent) {
+                if ($state instanceof Closure) {
+                    $state = $state->bindTo($this);
+                }
+                $mergeMe = $state($carry, $parent);
+
+                return array_merge($carry, $mergeMe);
+            },
+            $this->definition()
+        );
+
+        return $rawAttributes;
+    }
+
+    /**
+     * Create the parent relationship resolvers (as deferred Closures).
+     *
+     * @return array
+     */
+    protected function parentResolvers()
+    {
+        $model = $this->newModel();
+
+        $resolvers = [];
+        foreach ($this->for as $for) {
+            $resolvers[] = $for->attributesFor($model);
+        }
+
+        return $resolvers;
+    }
+
+    /**
+     * Expand all attributes to their underlying values.
+     *
+     * @param  array  $definition
+     * @return array
+     */
+    protected function expandAttributes(array $definition)
+    {
+        foreach ($definition as $key => $attribute) {
+            if (is_callable($attribute) && !is_string($attribute) && !is_array($attribute)) {
+                $attribute = $attribute($definition);
+            }
+
+            if ($attribute instanceof self) {
+                $attribute = $attribute->create()->getId();
+            } elseif ($attribute instanceof SimpleORMap) {
+                $attribute = $attribute->getId();
+            }
+
+            $definition[$key] = $attribute;
+        }
+
+        return $definition;
+    }
+
+    /**
+     * Add a new state transformation to the model definition.
+     *
+     * @param  callable|array  $state
+     * @return static
+     */
+    public function state($state)
+    {
+        $states = array_merge($this->states, [
+            is_callable($state)
+                ? $state
+                : function () use ($state) {
+                    return $state;
+                },
+        ]);
+
+        return $this->newInstance([
+            'states' => $states,
+        ]);
+    }
+
+    /**
+     * Add a new sequenced state transformation to the model definition.
+     *
+     * @param  array  $sequence
+     * @return static
+     */
+    public function sequence(...$sequence)
+    {
+        return $this->state(new Sequence(...$sequence));
+    }
+
+    /**
+     * Define a child relationship for the model.
+     *
+     * @param  \Studip\Database\Factories\Factory  $factory
+     * @param  string|null  $relationship
+     * @return static
+     */
+    public function has(self $factory, $relationship)
+    {
+        $has = array_merge($this->has, [new Relationship($factory, $relationship)]);
+
+        return $this->newInstance(['has' => $has]);
+    }
+
+    /**
+     * Get a new Faker instance.
+     *
+     * @return \Faker\Generator
+     */
+    protected function withFaker()
+    {
+        return \Faker\Factory::create();
+    }
+
+    /**
+     * Define a parent relationship for the model.
+     *
+     * @param  \Studip\Database\Factories\Factory|SimpleORMap  $factory
+     * @param  string  $relationship
+     * @return static
+     */
+    public function for($factory, $relationship)
+    {
+        $forWhom = array_merge($this->for, [new BelongsToRelationship($factory, $relationship)]);
+
+        return $this->newInstance(['for' => $forWhom]);
+    }
+
+    /**
+     * Add a new "after making" callback to the model definition.
+     *
+     * @param  \Closure  $callback
+     * @return static
+     */
+    public function afterMaking(Closure $callback)
+    {
+        $afterMaking = array_merge($this->afterMaking, [$callback]);
+
+        return $this->newInstance(['afterMaking' => $afterMaking]);
+    }
+
+    /**
+     * Add a new "after creating" callback to the model definition.
+     *
+     * @param  \Closure  $callback
+     * @return static
+     */
+    public function afterCreating(Closure $callback)
+    {
+        $afterCreating = array_merge($this->afterCreating, [$callback]);
+
+        return $this->newInstance(['afterCreating' => $afterCreating]);
+    }
+
+    /**
+     * Call the "after making" callbacks for the given model instances.
+     *
+     * @param  array  $instances
+     * @return void
+     */
+    protected function callAfterMaking(array $instances)
+    {
+        foreach ($instances as $model) {
+            foreach ($this->afterMaking as $callback) {
+                $callback($model);
+            }
+        }
+    }
+
+    /**
+     * Call the "after creating" callbacks for the given model instances.
+     *
+     * @param  array  $instances
+     * @param  SimpleORMap|null  $parent
+     * @return void
+     */
+    protected function callAfterCreating(array $instances, ?SimpleORMap $parent = null)
+    {
+        foreach ($instances as $model) {
+            foreach ($this->afterCreating as $callback) {
+                $callback($model, $parent);
+            }
+        }
+    }
+
+    /**
+     * Specify how many models should be generated.
+     *
+     * @param  int|null  $count
+     * @return static
+     */
+    public function count(?int $count)
+    {
+        return $this->newInstance(['count' => $count]);
+    }
+
+    /**
+     * Create a new instance of the factory builder with the given mutated properties.
+     *
+     * @param  array  $arguments
+     * @return static
+     */
+    protected function newInstance(array $arguments = [])
+    {
+        return new static(
+            ...array_values(
+                array_merge(
+                    [
+                        'count' => $this->count,
+                        'states' => $this->states,
+                        'has' => $this->has,
+                        'for' => $this->for,
+                        'afterMaking' => $this->afterMaking,
+                        'afterCreating' => $this->afterCreating,
+                    ],
+                    $arguments
+                )
+            )
+        );
+    }
+
+    /**
+     * Get a new model instance.
+     *
+     * @param  array  $attributes
+     * @return SimpleORMap
+     */
+    public function newModel(array $attributes = [])
+    {
+        $model = $this->modelName();
+
+        return $model::build($attributes);
+    }
+
+    /**
+     * Get the name of the model that is generated by the factory.
+     *
+     * @return string
+     */
+    public function modelName()
+    {
+        return $this->model;
+    }
+
+    /**
+     * Specify the default namespace that contains the application's model factories.
+     *
+     * @param  string  $namespace
+     * @return void
+     */
+    public static function useNamespace(string $namespace)
+    {
+        static::$namespace = $namespace;
+    }
+
+    /**
+     * Get a new factory instance for the given model name.
+     *
+     * @param  string  $modelName
+     * @return static
+     */
+    public static function factoryForModel(string $modelName)
+    {
+        $factory = static::resolveFactoryName($modelName);
+
+        return $factory::new();
+    }
+
+    /**
+     * Get the factory name for the given model name.
+     *
+     * @param  string  $modelName
+     * @return string
+     */
+    public static function resolveFactoryName(string $modelName)
+    {
+        return self::$namespace . $modelName . 'Factory';
+    }
+
+    /**
+     * Proxy dynamic factory methods onto their proper methods.
+     *
+     * @param  string  $method
+     * @param  array  $parameters
+     * @return mixed
+     */
+    public function __call($method, $parameters)
+    {
+        $slice = substr($method, 0, 3);
+        if (!in_array($slice, ['for', 'has'])) {
+            throw new \BadMethodCallException($method);
+        }
+    }
+}
diff --git a/lib/classes/Database/Factories/HasFactory.php b/lib/classes/Database/Factories/HasFactory.php
new file mode 100644
index 00000000000..c3a8a078573
--- /dev/null
+++ b/lib/classes/Database/Factories/HasFactory.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+trait HasFactory
+{
+    /**
+     * Get a new factory instance for the model.
+     *
+     * @param  mixed  $parameters
+     * @return \Studip\Database\Factories\Factory
+     */
+    public static function factory(...$parameters)
+    {
+        $factory = static::newFactory() ?: Factory::factoryForModel(get_called_class());
+
+        return $factory
+            ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : null)
+            ->state(is_array($parameters[0] ?? null) ? $parameters[0] : $parameters[1] ?? []);
+    }
+
+    /**
+     * Create a new factory instance for the model.
+     *
+     * @return \Studip\Database\Factories\Factory
+     */
+    protected static function newFactory()
+    {
+        //
+    }
+}
diff --git a/lib/classes/Database/Factories/Relationship.php b/lib/classes/Database/Factories/Relationship.php
new file mode 100644
index 00000000000..f3e7de2fde3
--- /dev/null
+++ b/lib/classes/Database/Factories/Relationship.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+use SimpleORMap;
+
+class Relationship
+{
+    /**
+     * The related factory instance.
+     *
+     * @var Factory
+     */
+    protected $factory;
+
+    /**
+     * The relationship name.
+     *
+     * @var string
+     */
+    protected $relationship;
+
+    /**
+     * Create a new child relationship instance.
+     *
+     * @param  Factory  $factory
+     * @param  string  $relationship
+     * @return void
+     */
+    public function __construct(Factory $factory, $relationship)
+    {
+        $this->factory = $factory;
+        $this->relationship = $relationship;
+    }
+
+    /**
+     * Create the child relationship for the given parent model.
+     *
+     * @param  SimpleORMap  $parent
+     * @return void
+     */
+    public function createFor(SimpleORMap $parent)
+    {
+        $relationship = $parent->getRelationOptions($this->relationship);
+
+        if (in_array($relationship['type'], ['has_one', 'has_many'])) {
+            $this->factory->state([
+                $relationship['assoc_foreign_key'] => $parent[$relationship['foreign_key']],
+            ])->create([], $parent);
+        } elseif (in_array($relationship['type'], ['belongs_to'])) {
+            $relationship->attach($this->factory->create([], $parent));
+        }
+    }
+}
diff --git a/lib/classes/Database/Factories/Sequence.php b/lib/classes/Database/Factories/Sequence.php
new file mode 100644
index 00000000000..faf69e34803
--- /dev/null
+++ b/lib/classes/Database/Factories/Sequence.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Studip\Database\Factories;
+
+class Sequence
+{
+    /**
+     * The sequence of return values.
+     *
+     * @var array
+     */
+    protected $sequence;
+
+    /**
+     * The count of the sequence items.
+     *
+     * @var int
+     */
+    public $count;
+
+    /**
+     * The current index of the sequence iteration.
+     *
+     * @var int
+     */
+    public $index = 0;
+
+    /**
+     * Create a new sequence instance.
+     *
+     * @param  array  $sequence
+     * @return void
+     */
+    public function __construct(...$sequence)
+    {
+        $this->sequence = $sequence;
+        $this->count = count($sequence);
+    }
+
+    /**
+     * Get the next value in the sequence.
+     *
+     * @return mixed
+     */
+    public function __invoke()
+    {
+        $value = $this->sequence[$this->index % $this->count];
+        $result = $value instanceof \Closure ? $value($this) : $value;
+        $this->index = $this->index + 1;
+
+        return $result;
+    }
+}
diff --git a/lib/classes/Database/Seeder.php b/lib/classes/Database/Seeder.php
new file mode 100644
index 00000000000..fe122873c87
--- /dev/null
+++ b/lib/classes/Database/Seeder.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Studip\Database;
+
+use InvalidArgumentException;
+
+abstract class Seeder
+{
+    /**
+     * Run the database seeds.
+     *
+     * @param  array  $parameters
+     * @return mixed
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function __invoke(array $parameters = [])
+    {
+        if (!method_exists($this, 'run')) {
+            throw new InvalidArgumentException('Method [run] missing from ' . get_class($this));
+        }
+
+        return $this->run(...$parameters);
+    }
+}
diff --git a/lib/classes/Database/Seeders/ActivitiesSeeder.php b/lib/classes/Database/Seeders/ActivitiesSeeder.php
new file mode 100644
index 00000000000..83a887b2cd0
--- /dev/null
+++ b/lib/classes/Database/Seeders/ActivitiesSeeder.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Studip\Database\Seeders;
+
+use Studip\Database\Seeder;
+
+class ActivitiesSeeder extends Seeder
+{
+    /**
+     * Run the database seeders.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        $course = \Course::find('a07535cf2f8a72df33c12ddfa4b53dde');
+        if (!$course) {
+            throw new \RuntimeException('Could not find course.');
+        }
+
+        $students = \User::factory()
+            ->count(35)
+            ->afterMaking(function (\User $user) use ($course) {
+                $user->course_memberships[] = \CourseMember::build([
+                    'Seminar_id' => $course->getId(),
+                    'user_id' => $user->getId(),
+                    'status' => 'autor',
+                ]);
+            })
+            ->create();
+
+        $faker = \Faker\Factory::create();
+
+        foreach ($students as $student) {
+            self::withUser($student, function ($student) use ($faker, $course) {
+                if (rand() / getrandmax() < 0.25) {
+                    // add wiki page
+                    \WikiPage::create([
+                        'range_id' => $course->getId(),
+                        'user_id' => $student->getId(),
+                        'keyword' => $faker->words(2, true),
+                        'body' => $faker->sentence(3),
+                        'ancestor' => 'WikiWikiWeb',
+                        'version' => 0,
+                    ]);
+                }
+
+                // send message
+                // enter course
+                // leave course
+                // create file
+                // update file
+                // delete file
+                // create news
+                // add wiki page
+                // delete wiki page
+                // CourseDidChangeSchedule???
+            });
+        }
+    }
+
+    private static function withUser(\User $user, callable $callable)
+    {
+        $oldUser = $GLOBALS['user'];
+        $GLOBALS['user'] = new \Seminar_User($user);
+
+        $result = $callable($user);
+
+        $GLOBALS['user'] = $oldUser;
+
+        return $result;
+    }
+}
diff --git a/lib/classes/Database/Seeders/CourseSeeder.php b/lib/classes/Database/Seeders/CourseSeeder.php
new file mode 100644
index 00000000000..b32d90c6b66
--- /dev/null
+++ b/lib/classes/Database/Seeders/CourseSeeder.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Studip\Database\Seeders;
+
+use Studip\Database\Seeder;
+
+class CourseSeeder extends Seeder
+{
+    /**
+     * Run the database seeders.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        /** @var ?\User $testDozent */
+        $testDozent = \User::findByUsername('test_dozent');
+        if (!$testDozent) {
+            throw new \RuntimeException('Cannot create course without `test_dozent`.');
+        }
+        $members = [\CourseMember::build(['user_id' => $testDozent->getId(), 'status' => 'dozent'])];
+
+        $course = \Course::factory()
+            ->count(200)
+            ->afterMaking(function (\Course $course) use ($testDozent) {
+                $course->members[] = \CourseMember::build([
+                    'Seminar_id' => $course->getId(),
+                    'user_id' => $testDozent->getId(),
+                    'status' => 'dozent',
+                ]);
+            })
+            ->create();
+
+        // $students = \User::factory()
+        //     ->count(35)
+        //     ->afterMaking(function (\User $user) use ($course) {
+        //         $user->course_memberships[] = \CourseMember::build([
+        //             'Seminar_id' => $course->getId(),
+        //             'user_id' => $user->getId(),
+        //             'status' => 'autor',
+        //         ]);
+        //     })
+        //     ->create();
+    }
+}
diff --git a/lib/classes/Database/Seeders/DatabaseSeeder.php b/lib/classes/Database/Seeders/DatabaseSeeder.php
new file mode 100644
index 00000000000..506136f27e4
--- /dev/null
+++ b/lib/classes/Database/Seeders/DatabaseSeeder.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Studip\Database\Seeders;
+
+use Studip\Database\Seeder;
+
+class DatabaseSeeder extends Seeder
+{
+    /**
+     * Seed the application's database.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        var_dump(__FILE__, __LINE__);
+        // \App\Models\User::factory(10)->create();
+    }
+}
diff --git a/lib/classes/Database/Seeders/ExampleSeeder.php b/lib/classes/Database/Seeders/ExampleSeeder.php
new file mode 100644
index 00000000000..992ba00b182
--- /dev/null
+++ b/lib/classes/Database/Seeders/ExampleSeeder.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Studip\Database\Seeders;
+
+use Studip\Database\Seeder;
+
+class ExampleSeeder extends Seeder
+{
+    /**
+     * Run the database seeders.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        $course = \Course::find('a07535cf2f8a72df33c12ddfa4b53dde');
+        if (!$course) {
+            throw new \RuntimeException('Could not find course.');
+        }
+        $instance = \Courseware\StructuralElement::createEmptyCourseware($course->getId(), 'course');
+        $parent = $instance->getRoot();
+
+        $students = \User::factory()
+            ->count(35)
+            ->afterMaking(function (\User $user) use ($course) {
+                $user->course_memberships[] = \CourseMember::build([
+                    'Seminar_id' => $course->getId(),
+                    'user_id' => $user->getId(),
+                    'status' => 'autor',
+                ]);
+            })
+            ->create();
+
+        $lvl1 = \Courseware\StructuralElement::factory()
+            ->count(25)
+            ->withParent($parent)
+            ->create();
+
+        foreach ($lvl1 as $parent1) {
+            $lvl2 = \Courseware\StructuralElement::factory()
+                ->count(12)
+                ->withParent($parent1)
+                ->create();
+
+            foreach ($lvl2 as $parent2) {
+                $lvl3 = \Courseware\StructuralElement::factory()
+                    ->count(1)
+                    ->withParent($parent2)
+                    ->has($this->containersWithBlocks(), 'containers')
+                    ->create();
+            }
+        }
+
+        foreach ($instance->findAllBlocks() as $block) {
+            foreach ($students as $student) {
+                if (mt_rand(0, 1)) {
+                    \Courseware\UserProgress::create([
+                        'user_id' => $student->getId(),
+                        'block_id' => $block->getId(),
+                        'grade' => 1,
+                    ]);
+                }
+            }
+        }
+    }
+
+    private function containersWithBlocks(): \Studip\Database\Factories\Factory
+    {
+        return \Courseware\Container::factory()
+            ->count(2)
+            ->state(function (array $attributes, \Courseware\StructuralElement $parent) {
+                return [
+                    'owner_id' => $parent->owner_id,
+                    'editor_id' => $parent->editor_id,
+                ];
+            })
+            ->has(
+                \Courseware\Block::factory()
+                    ->count(10)
+                    ->state(function (array $attributes, \Courseware\Container $parent) {
+                        return [
+                            'owner_id' => $parent->owner_id,
+                            'editor_id' => $parent->editor_id,
+                        ];
+                    }),
+                'blocks'
+            );
+    }
+}
diff --git a/lib/models/SimpleORMap.class.php b/lib/models/SimpleORMap.class.php
index 8f951b4abc3..5ba2157ce11 100644
--- a/lib/models/SimpleORMap.class.php
+++ b/lib/models/SimpleORMap.class.php
@@ -1,4 +1,7 @@
 <?php
+
+use Studip\Database\Factories\HasFactory;
+
 /**
  * SimpleORMap.class.php
  * simple object-relational mapping
@@ -16,6 +19,8 @@
 
 class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate
 {
+    use HasFactory;
+
     /**
      * Defines `_` as character used when joining composite primary keys.
      */
diff --git a/lib/models/factories/CourseFactory.php b/lib/models/factories/CourseFactory.php
new file mode 100644
index 00000000000..d9d5fbb4106
--- /dev/null
+++ b/lib/models/factories/CourseFactory.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Factories;
+
+use Studip\Database\Factories\Factory;
+
+class CourseFactory extends Factory
+{
+    protected $model = \Course::class;
+
+    public function definition()
+    {
+        return [
+            'Seminar_id' => $this->faker->md5(),
+            // VeranstaltungsNummer
+            // ! Institut_id
+            'Name' => $this->faker->sentence(3),
+            // Untertitel
+            'status' => 1, // TODO: this should be something else I guess
+            // Beschreibung
+            // Ort
+            // Sonstiges
+            'Lesezugriff' => 1,
+            'Schreibzugriff' => 1,
+            'start_time' => \Semester::findCurrent()->beginn,
+            'duration_time' => 0,
+            // art
+            // teilnehmer
+            // vorrausetzungen
+            // lernorga
+            // leistungsnachweis
+            // - mkdate
+            // - chdate
+            // - ects
+            'admission_turnout' => 0,
+            'admission_binding' => 0,
+            'admission_prelim' => 0,
+            // 'admission_prelim_txt' => null,
+            'admission_disable_waitlist' => 0,
+            'visible' => 1,
+            'showscore' => 0,
+            // 'aux_lock_rule' => null,
+            'aux_lock_rule_forced' => 0,
+            // 'lock_rule' => null,
+            'admission_waitlist_max' => 0,
+            'admission_disable_waitlist_move' => 0,
+            'completion' => 0,
+            // 'parent_course' => null,
+        ];
+    }
+}
diff --git a/lib/models/factories/Courseware/BlockFactory.php b/lib/models/factories/Courseware/BlockFactory.php
new file mode 100644
index 00000000000..da23a569e00
--- /dev/null
+++ b/lib/models/factories/Courseware/BlockFactory.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Factories\Courseware;
+
+use Studip\Database\Factories\Factory;
+
+class BlockFactory extends Factory
+{
+    protected $model = \Courseware\Block::class;
+
+    public function definition()
+    {
+        return [
+            'block_type' => 'text',
+            'visible' => true,
+
+            // `position` int(11) NOT NULL,
+            // `payload` MEDIUMTEXT NOT NULL,
+
+            // `container_id` int(11) NOT NULL,
+            // `owner_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            // `editor_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+            // `edit_blocker_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NULL,
+
+            // `mkdate` int(11) NOT NULL,
+            // `chdate` int(11) NOT NULL,
+        ];
+    }
+}
diff --git a/lib/models/factories/Courseware/ContainerFactory.php b/lib/models/factories/Courseware/ContainerFactory.php
new file mode 100644
index 00000000000..1744da9c975
--- /dev/null
+++ b/lib/models/factories/Courseware/ContainerFactory.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Factories\Courseware;
+
+use Courseware\Container;
+use Courseware\StructuralElement;
+use Studip\Database\Factories\Factory;
+
+class ContainerFactory extends Factory
+{
+    protected $model = Container::class;
+
+    public function definition()
+    {
+        return [
+            'container_type' => 'list',
+            // structural_element_id
+            // owner_id
+            // editor_id
+            // edit_blocker_id
+            // position
+            // site
+            // container_type
+            // visible
+            // payload
+            // mkdate
+            // chdate
+        ];
+    }
+
+    public function withElement(StructuralElement $element): Factory
+    {
+        return $this->state(function (array $attributes) use ($element) {
+            return [
+                'structural_element_id' => $element->getId(),
+                'owner_id' => $element['owner_id'],
+                'editor_id' => $element['owner_id'],
+            ];
+        });
+    }
+}
diff --git a/lib/models/factories/Courseware/StructuralElementFactory.php b/lib/models/factories/Courseware/StructuralElementFactory.php
new file mode 100644
index 00000000000..3451d514fb1
--- /dev/null
+++ b/lib/models/factories/Courseware/StructuralElementFactory.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Factories\Courseware;
+
+use Courseware\StructuralElement;
+use Studip\Database\Factories\Factory;
+
+class StructuralElementFactory extends Factory
+{
+    protected $model = StructuralElement::class;
+
+    public function definition()
+    {
+        return [
+            'title' => $this->faker->sentence(),
+
+            // parent_id
+            // range_id
+            // range_type
+            // owner_id
+            // editor_id
+            // edit_blocker_id
+            // position
+            // title
+            // image_id
+            // purpose
+            // payload
+            // public
+            // release_date
+            // withdraw_date
+            // read_approval
+            // write_approval
+            // copy_approval
+            // external_relations
+            // mkdate
+            // chdate
+        ];
+    }
+
+    public function withParent(StructuralElement $parent): Factory
+    {
+        return $this->state(function (array $attributes) use ($parent) {
+            return [
+                'parent_id' => $parent->getId(),
+                'range_id' => $parent['range_id'],
+                'range_type' => $parent['range_type'],
+                'owner_id' => $parent['owner_id'],
+                'editor_id' => $parent['owner_id'],
+            ];
+        });
+    }
+}
diff --git a/lib/models/factories/UserFactory.php b/lib/models/factories/UserFactory.php
new file mode 100644
index 00000000000..bbc49efc511
--- /dev/null
+++ b/lib/models/factories/UserFactory.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Factories;
+
+use Studip\Database\Factories\Factory;
+
+class UserFactory extends Factory
+{
+    protected $model = \User::class;
+
+    public function definition()
+    {
+        return [
+            'username' => $this->faker->userName(),
+            // `password` varbinary(64) NOT NULL DEFAULT '',
+            'perms' => 'autor',
+            'Vorname' => $this->faker->firstName(),
+            'Nachname' => $this->faker->lastName(),
+            'Email' => $this->faker->email(),
+            // `validation_key` varchar(10) NOT NULL DEFAULT '',
+            // `auth_plugin` varchar(64) DEFAULT 'standard',
+            // `locked` tinyint(1) UNSIGNED NOT NULL DEFAULT '0',
+            // `lock_comment` varchar(255) DEFAULT NULL,
+            // `locked_by` varchar(32) DEFAULT NULL,
+            'visible' => 'yes',
+        ];
+    }
+
+    public function invisible(): Factory
+    {
+        return $this->state(function (array $attributes) {
+            return ['visible' => 'no'];
+        });
+    }
+}
diff --git a/tests/jsonapi/ModelFactoryTest.php b/tests/jsonapi/ModelFactoryTest.php
new file mode 100644
index 00000000000..adb593b5181
--- /dev/null
+++ b/tests/jsonapi/ModelFactoryTest.php
@@ -0,0 +1,224 @@
+<?php
+
+use Studip\Database\Factories\Sequence;
+
+class ModelFactoryTest extends \Codeception\Test\Unit
+{
+    /**
+     * @var \UnitTester
+     */
+    protected $tester;
+
+    protected function _before()
+    {
+        \DBManager::getInstance()->setConnection('studip', $this->getModule('\\Helper\\StudipDb')->dbh);
+    }
+
+    protected function _after()
+    {
+    }
+
+    // tests
+
+    /**
+     * @group specification
+     */
+    public function testSORMClassesShouldProvideAFactory()
+    {
+        $factory = \User::factory();
+        $this->assertInstanceOf(\Studip\Database\Factories\Factory::class, $factory);
+        $this->assertInstanceOf(\Studip\Database\Factories\UserFactory::class, $factory);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeAnInstance()
+    {
+        $user = \User::factory()->make();
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertEquals('autor', $user['perms']);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeAnInstanceWithMoreAttributes()
+    {
+        $user = \User::factory()->make(['username' => 'foobar']);
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertEquals('foobar', $user['username']);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeAnInstanceWithInlineState()
+    {
+        $user = \User::factory()
+            ->state(['username' => 'bazbar'])
+            ->make();
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertEquals('bazbar', $user['username']);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeANumberOfInstances()
+    {
+        $users = \User::factory()
+            ->count(3)
+            ->make();
+        $this->assertIsArray($users);
+        $this->assertCount(3, $users);
+        $this->assertContainsOnlyInstancesOf(\User::class, $users);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldCreateAnInstanceInTheDatabase()
+    {
+        $countUsers = \User::countBySql('1');
+
+        $user = \User::factory()->create();
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertFalse($user->isNew());
+
+        $this->assertEquals($countUsers + 1, \User::countBySql('1'));
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldCreateAnInstanceInTheDatabaseWithMoreAttributes()
+    {
+        $user = \User::factory()->create(['username' => 'foobaz']);
+        $this->assertEquals('foobaz', $user['username']);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldCreateANumberOfInstancesInTheDatabase()
+    {
+        $countUsers = \User::countBySql('1');
+
+        $users = \User::factory()
+            ->count(3)
+            ->create();
+        $this->assertIsArray($users);
+
+        $this->assertEquals($countUsers + 3, \User::countBySql('1'));
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldAllowToCallStateMethods()
+    {
+        $user = \User::factory()
+            ->invisible()
+            ->make();
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertEquals('no', $user['visible']);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldAllowAfterMakingCallbacks()
+    {
+        $visited = false;
+        $user = \User::factory()
+            ->afterMaking(function (\User $user) use (&$visited) {
+                $visited = true;
+            })
+            ->make();
+        $this->assertInstanceOf(\User::class, $user);
+        $this->assertTrue($visited);
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeANumberOfInstancesUsingASequence()
+    {
+        $usernames = [
+            ['username' => 'deathborgdelta'],
+            ['username' => 'deathborgzeta'],
+            ['username' => 'deathborgtheta'],
+        ];
+        $users = \User::factory()
+            ->count(5)
+            ->state(new Sequence(...$usernames))
+            ->make();
+        $this->assertIsArray($users);
+        $this->assertCount(5, $users);
+        $this->assertContainsOnlyInstancesOf(\User::class, $users);
+        for ($i = 0; $i < 3; $i++) {
+            $this->assertEquals($usernames[$i]['username'], $users[$i]['username']);
+        }
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeANumberOfInstancesUsingACallableSequence()
+    {
+        $usernames = [
+            ['username' => 'deathborgdelta'],
+            ['username' => 'deathborgzeta'],
+            ['username' => 'deathborgtheta'],
+        ];
+        $users = \User::factory()
+            ->count(5)
+            ->state(
+                new Sequence(function ($sequence) use ($usernames) {
+                    return isset($usernames[$sequence->index]) ? $usernames[$sequence->index] : [];
+                })
+            )
+            ->make();
+        $this->assertIsArray($users);
+        $this->assertCount(5, $users);
+        $this->assertContainsOnlyInstancesOf(\User::class, $users);
+        for ($i = 0; $i < 3; $i++) {
+            $this->assertEquals($usernames[$i]['username'], $users[$i]['username']);
+        }
+    }
+
+    /**
+     * @group specification
+     */
+    public function testShouldMakeANumberOfInstancesUsingSequenceMethod()
+    {
+        $usernames = [
+            ['username' => 'deathborgdelta'],
+            ['username' => 'deathborgzeta'],
+            ['username' => 'deathborgtheta'],
+        ];
+        $users = \User::factory()
+            ->count(5)
+            ->sequence(function ($sequence) use ($usernames) {
+                return isset($usernames[$sequence->index]) ? $usernames[$sequence->index] : [];
+            })
+            ->make();
+        $this->assertIsArray($users);
+        $this->assertCount(5, $users);
+        $this->assertContainsOnlyInstancesOf(\User::class, $users);
+        for ($i = 0; $i < 3; $i++) {
+            $this->assertEquals($usernames[$i]['username'], $users[$i]['username']);
+        }
+    }
+
+    /**
+     * @group specification
+     */
+    public function ytestShouldMakeBlocksWithAnOwner()
+    {
+        $block = \Courseware\Block::factory()
+            ->for(\User::factory(), 'owner')
+            ->make();
+    }
+}
-- 
GitLab