未加星标

Adding TypeScript definitions to factory-girl

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2018 | 作者 红领巾 ] 0人收藏点击收藏

The javascript ecosystem has many libraries available. In fact, npm has over 350,000 packages and counting, which is a considerable quantity of code. The problem comes mainly because most of these have been done in plain Javascript, and they haven't adopted the type safety of TypeScript. At Stack Builders we embrace type safety since it makes the code more expressive and maintainable in the long-term. It's because of this that today we present to you our type contribution to the factory-girl library, with a previous explanation about why is it important and how we tackled the problem of adding types to it.

The Factory Method pattern

When writing automated tests every developer will inevitably need to generate fake data. The issue at hand comes when we need to test a class or a Javascript object, and we depend on a concrete implementation of it. This makes the test case and the class tightly coupled, because said class can change its constructor over time causing the tests to break. To solve this we can rely on the factory method pattern , which is done by calling a factory method instead of the specific class constructors to generate concrete objects.

There are many libraries for the Javascript ecosystem that help us implement this pattern, but in this article, we'll focus on one in particular.

Hello, factory-girl!

factory-girl is a popular Javascript factory library, inspired by Ruby's factory-bot (it was formerly known as factory-girl in the ruby ecosystem as well, but they changed the name in 2017 for a few reasons described here ). This library allows us to generate factory methods to be used in our tests. Consider the following example:

const factory = require("factory-girl").factory; const ToughGuy = require("./models/toughguy"); factory.define("tough-guy", ToughGuy, { firstName: "John", lastName: "Rambo", email: factory.seq( "ToughGuy.email", (n) => `guy${n}@fbi.com` ) });

Here we defined a factory method for a ToughGuy model class, which allows us to repeatedly create it for our tests. If we use it as is, it will always create it setting firstName to "John" and lastName to "Rambo". The email field uses the factory.seq method, so when we generate the first object with this factory it will set the field to guy1@fbi.com . The second object will be generated with guy2@fbi.com , and so on. This factory will be used as follows:

factory.build("tough-guy").then(guy => { // Factory returns a promisified result. // In this case "guy" will be: // { // firstName: "John", // lastName: "Rambo", // email: "guy1@fbi.com" // } }) factory.build( "tough-guy", { lastName: "Wick" } ).then(guy => { // The data for the factory can be overwritten. // In this case "guy" will be // { // firstName: "John", // lastName: "Wick", // email: "guy2@fbi.com" // } })

With this approach we can generate as many ToughGuy instances as we need without having to create them using the class constructor. This brings a second problem to the table, which is...

Where are my types?

The dynamic nature of Javascript allows us to create these objects easily without worrying too much about the individual types for the class attributes. Because of this we could potentially do the following:

factory.build( "tough-guy", { lastName: true, quote: "Hahaha!" } ).then(guy => { // Here "guy" will be: // { // firstName: "John", // lastName: true, // email: "guy1@fbi.com" // } })

As you can see, factory-girl doesn't care at all about the primitive types that the factory method generates. Moreover, it doesn't care that we sent a quote attribute that isn't on the || ToughGuy definition. This could potentially cause us to write tests that don't check an object thoroughly and lead us to test cases that shouldn't even exist. To solve this we need to bring another friend to the discussion.

TypeScript to the rescue

TypeScript helps us bring some of the benefits of the static analysis to the table, so we could define better semantics and catch some trivial errors like typos or extra fields. Let's rewrite our example using it:

// The ToughGuy class doesn't have types, // so we would use an interface to force // every ToughGuy object to adhere to // specific types // // interface ToughGuy { // firstName: string; // lastName: string; // email: string; // } factory.build( "tough-guy", { lastName: true } ).then(guy => { // Here "guy" will be: // { // firstName: "John", // lastName: true, // email: "guy1@fbi.com" // } })

So this gives us the same output from last time. It didn't check types at all, even though we wrote it using TypeScript. Why isn't this adhering to the ToughGuy interface that we defined earlier? The answer is simple: The factory-girl library doesn't have type definitions.

Adding type definitions to factory-girl

DefinitelyTyped contains external type definitions for libraries that are written in plain JS. Sadly there weren't type definitions for factory-girl, which made the factory methods a bit unsafe for testing. In Stack Builders we recently added the types for this library . Let's take a look at the definition for factory.define and factory.build :

define<T>( name: string, model: any, attrs: T, options?: Options<T> ): void; build<T>( name: string, attrs?: Partial<T> ): Promise<T>;

So what does this mean? In short, define is tied to a generic type T which forces us to create an object with the attributes in it. Going back to our example we can be completely sure that when defining a factory for a ToughGuy we only use the attributes defined the ToughGuy interface.

So what about the build function? Let's dig in a bit deeper: - build<T> tells us that when using this function we need to send a type to it, which will be tied to the function's arguments and return values - name: string just gives a type to the factory name. Nothing too complicated here - attrs?: Partial<T> is the important part. Here we're telling the compiler to use the generic T type defined at the start. So in this case we won't be able to send additional attributes that are not in the original type or an object with the wrong types. - After building the object we get a promisified result of the original T type, which is defined by : Promise<T>

When using these type definitions we need to be strict with the things we send to our build method, since otherwise the TypeScript compiler will complain. This brings additional safety to the factory functions and lets us write more consistent tests.

But wait... What does that Partial mean?

Partial types were introduced in TypeScript 2.1. Under the hood a partial is an alias that looks similar to this:

type Partial<T> = { [P in keyof T]?: T; };

This Partial type flags all properties in T as optional, but you are still allowed to use all of the type's properties if needed. Let's check its behavior in our previous example:

// Let's assume for a moment that the type // definition for the "build" function is: build<T>(name: string, attrs?: T): Promise<T>; interface ToughGuy { firstName: string; lastName: string; email: string; } factory.build<ToughGuy>( "tough-guy", { lastName: true } ).then(guy => { // The code will not compile, since the object // we're sending in the attributes parameter // doesn't have a `firstName` or an `email` // in its properties. It will also fail because // `lastName` should be a string, not a boolean })

The function definition expects an object that's fully compliant with T , but in this case we only need to send a subset of the type's properties. In versions earlier to 2.1 we could do something like this:

interface ToughGuy { firstName: string; lastName: string; email: string; } // Define another interface with all optional // attributes interface ToughGuyPartial { firstName?: string; lastName?: string; email?: string; } // Then our type definition would be something // like this build<T, U>( name: string, attrs?: U ): Promise<T>; // Which would be used like this factory.build<ToughGuy, ToughGuyPartial>( "tough-guy", { lastName: "McClane" } ).then(guy => { // ... });

So partials let us reuse a type and have only a subset of that type's properties. Our original example would be:

// Our type definition is build<T>( name: string, attrs?: Partial<T> ): Promise<T>; // Usage would be factory.build<ToughGuy>( "tough-guy", { lastName: "McClane" } ).then(guy => { // Compiles correctly! });

In conclusion, adding TypeScript types to an existing library is a great way to make it more robust and safe. You can see a usage example for this library and its types in this repository . We also encourage you to check your favorite libraries for types in the DefinitelyTyped repository, and to add your own types if there are none. This will help you and all of the library's users to have an additional safety check so the bugs can be minimized.

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:Adding TypeScript definitions to factory-girl
本站链接:https://www.codesec.net/view/610973.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(57)