I'm having trouble setting up tests for a component that relies on a redux store passed down from context...
My app has a root component that gives its children the redux store as context:
import ApplicationStore from './app-store.js'
class Root extends Component {
getChildContext() {
return { store: ApplicationStore }
}
render(){...}
}
Root.childContextTypes = {
store: PropTypes.object,
};
I have a component that depends on the store (passed down from context):
class List extends Component {
render(){
const items = this.context.store.getState();
return items.map((_, i) => (
<div key={i}>{_.name}</div>
));
}
}
List.contextTypes = {
store: PropTypes.object,
};
So my question is: How to I "inject" the store object into my component's context? Would I have to unmock(./app-store.js)? Additionally, how can I pre-fill the store with a couple fixtures?
Thanks!
I know this does not answer your question, but I would like to point you to the React.js documentation on contexts:
Most applications will never need to use context. Especially if you are just getting started with React, you likely do not want to use context. Using context will make your code harder to understand because it makes the data flow less clear. It is similar to using global variables to pass state through your application.
and
Do not use context to pass your model data through components. Threading your data through the tree explicitly is much easier to understand. Using context makes your components more coupled and less reusable, because they behave differently depending on where they're rendered.
If you use props to pass your store (or even only the required parts of it) to your child components, then your problem is already solved because in your test, you can simply inject your mocked or stubbed store.
Related
In a fresh Ember 3.20 project (also same in 3.19), after adding a new component.
application.hbs:
<Test #foo="hello" />
components/test.js:
import Component from '#glimmer/component';
export default class TestComponent extends Component {
constructor(){
super(...arguments)
console.log(this.args)
}
}
Console: Proxy {}
How can I access this.args within the constructor?
the args of a component is a Proxy so that special behavior can wrap accesses to the args. For example, when you access one of the args -- only then will the arg be "consumed" and your getters/actions entangled with the state of that arg. This has the advantage of optimizing your change tracking by default, so if you pass 100 args, and only use one or two of them in your component, you don't have to pay the cost of those 100 args causing updates to your component.
It works kinda like this:
args = new Proxy(actualArgs, {
get(target, argName) {
consumeTag(tagFor(target, argName));
return target[argName];
}
});
Where, normally, if you only had a vanilla object, accessing the arg would only get you the value. This proxy demonstrates interaction with the tracking system so that your component can now be entangled with the arg's updates
For more information on autotracking, #pzuraq goes in to great depth here: https://www.pzuraq.com/how-autotracking-works/ (not necessarily auto-tracking in ember, but more in general)
Also, documentation on Proxy, if interested: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Note: this information is for Ember 3.13+
I've been experimenting with GraphQL/Apollo in Typescript and run into a huge amount of really obvious typing issues that make me wonder whether I'm just doing it wrong.
Effectively, what I'd like to do (since it makes sense to me) is compose the data for my components by plastering in multiple HOC calls. Here's an example of what I mean (sans imports)
interface Props {
theme: Theme;
}
class Navigation extends React.Component<Props & ViewerProps & PageProps> {
render() {
const { viewer, page } = this.props;
return <h1>Hello, {viewer.name}; welcome to {page.name}</h1>;
}
}
export default withPage(withViewer(Navigation));
Elsewhere:
<Navigation theme={this.theme} />
Is this actually a pattern of any kind in Apollo? Are there any references out there which use this, or something similar?
Thanks
In Apollo 2.1 they upgraded to use render prop functions.
https://reactjs.org/docs/render-props.html which will help with stronger typing through the components.
https://dev-blog.apollodata.com/introducing-react-apollo-2-1-c837cc23d926
One thing I've been doing with components with HoCs is to cast the HoC with React.ComponentType so that it can make sure you're passing in the correct props to the component
const NavigationContainer: React.ComponentType<Props> =
withPage(withViewer(Navigation));
export default NavigationContainer;
React doesn't provide an API that lets you pass in context to a created component class, so you have to write a wrapper component that provides the context.
Unfortunately, once you do this, you no longer have direct access to the component you are trying to test - unlike TestUtils.renderIntoDocument, functions like TestUtils.findRenderedComponentWithType don't return the actual rendered component instance, they only return the component constructor. Thus you can't call methods on the component, or set the component state to some known value before executing the test. The only thing you really have access to is the DOM node for your component, which is fine if all you want to do is black box testing, but for some kinds of components that's not sufficient.
I'm curious to know if anyone has come up with a solution for this. I've tried about a dozen different approaches, none of which work. (For example, I tried using 'ref' in my wrapper component, but it has the same problem - doesn't give you access to the real object.)
(Answering my own question)
Turns out the correct answer to all of this is to use enzyme, which replaces the standard React test utils - it offers a ton of features with a jQuery-like API, and best of all it completely supports component contexts. I've switched all of my tests over to it and it's great!
You can build a mock parent component like:
class MockContextContainer extends Component {
static propTypes = {
children: PropTypes.element.isRequired,
};
static childContextTypes = {
onSetTitle: PropTypes.func,
};
getChildContext() {
return {
onSetTitle: () => {},
};
}
render() {
return this.props.children;
}
}
Then use it in your test (in this case its a forgot password form example):
const cxtForgot = TestUtils.renderIntoDocument(
<MockContextContainer><ForgotPasswordForm /></MockContextContainer>
);
Which is what you may already be doing.
You can then do things like:
const input = TestUtils.findRenderedDOMComponentWithClass(
cxtForgot, 'ForgotPasswordForm-input'
);
// enter a valid email
input.value = 'abc#hotmail.com';
TestUtils.Simulate.change(input);
// no error class and button is now enabled
assert(!input.classList.contains('error'));
const button1 = TestUtils.findRenderedDOMComponentWithClass(
cxtForgot, 'primary-button'
);
assert(!button1.disabled);
the Simulate.change above can change the internal state of the component.
As for you question: "set the component state to some known value before executing the test", you can pass in different props to the component and have different tests for each scenario
I'm mocking my repository correctly, but in cases like show() it either returns null so the view ends up crashing the test because of calling property on null object.
I'm guessing I'm supposed to mock the eloquent model returned but I find 2 issues:
What's the point of implementing repository pattern if I'm gonna end up mocking eloquent model anyway
How do you mock them correctly? The code below gives me an error.
$this->mockRepository->shouldReceive('find')
->once()
->with(1)
->andReturn(Mockery::mock('MyNamespace\MyModel)
// The view may call $book->title, so I'm guessing I have to mock
// that call and it's returned value, but this doesn't work as it says
// 'Undefined property: Mockery\CompositeExpectation::$title'
->shouldReceive('getAttribute')
->andReturn('')
);
Edit:
I'm trying to test the controller's actions as in:
$this->call('GET', 'books/1'); // will call Controller#show(1)
The thing is, at the end of the controller, it returns a view:
$book = Repo::find(1);
return view('books.show', compact('book'));
So, the the test case also runs view method and if no $book is mocked, it is null and crashes
So you're trying to unit test your controller to make sure that the right methods are called with the expected arguments. The controller-method fetches a model from the repo and passes it to the view. So we have to make sure that
the find()-method is called on the repo
the repo returns a model
the returned model is passed to the view
But first things first:
What's the point of implementing repository pattern if I'm gonna end up mocking eloquent model anyway?
It has many purposes besides (testable) consisten data access rules through different sources, (testable) centralized cache strategies, etc. In this case, you're not testing the repository and you actually don't even care what's returned, you're just interested that certain methods are called. So in combination with the concept of dependency injection you now have a powerful tool: You can just switch the actual instance of the repo with the mock.
So let's say your controller looks like this:
class BookController extends Controller {
protected $repo;
public function __construct(MyNamespace\BookRepository $repo)
{
$this->repo = $repo;
}
public function show()
{
$book = $this->repo->find(1);
return View::make('books.show', compact('book'));
}
}
So now, within your test you just mock the repo and bind it to the container:
public function testShowBook()
{
// no need to mock this, just make sure you pass something
// to the view that is (or acts like) a book
$book = new MyNamespace\Book;
$bookRepoMock = Mockery::mock('MyNamespace\BookRepository');
// make sure the repo is queried with 1
// and you want it to return the book instanciated above
$bookRepoMock->shouldReceive('find')
->once()
->with(1)
->andReturn($book);
// bind your mock to the container, so whenever an instance of
// MyNamespace\BookRepository is needed (like in your controller),
// the mock will be loaded.
$this->app->instance('MyNamespace\BookRepository', $bookRepoMock);
// now trigger the controller method
$response = $this->call('GET', 'books/1');
$this->assertEquals(200, $response->getStatusCode());
// check if the controller passed what was returned from the repo
// to the view
$this->assertViewHas('book', $book);
}
//EDIT in response to the comment:
Now, in the first line of your testShowBook() you instantiate a new Book, which I am assuming is a subclass of Eloquent\Model. Wouldn't that invalidate the whole deal of inversion of control[...]? since if you change ORM, you'd still have to change Book so that it wouldn't be class of Model
Well... yes and no. Yes, I've instantiated the model-class in the test directly, but model in this context doesn't necessarily mean instance of Eloquent\Model but more like the model in model-view-controller. Eloquent is only the ORM and has a class named Model that you inherit from, but the model-class as itself is just an entity of the business logic. It could extend Eloquent, it could extend Doctrine, or it could extend nothing at all.
In the end it's just a class that holds the data that you pull e.g. from a database, from an architecture point of view it is not aware of any ORM, it just contains data. A Book might have an author attribute, maybe even a getAuthor() method, but it doesn't really make sense for a book to have a save() or find() method. But it does if you're using Eloquent. And it's ok, because it's convenient, and in small project there's nothing wrong with accessing it directly. But it's the repository's (or the controller's) job to deal with a specific ORM, not the model's. The actual model is sort of the outcome of an ORM-interaction.
So yes, it might be a little confusing that the model seems so tightly bound to the ORM in Laravel, but, again, it's very convenient and perfectly fine for most projects. In fact, you won't even notice it unless you're using it directly in your application code (e.g. Book::where(...)->get();) and then decide to switch from Eloquent to something like Doctrine - this would obviously break your application. But if this is all encapsulated behind a repository, the rest of your application won't even notice when you switch between databases or even ORMs.
So, you're working with repositories, so only the eloquent-implementation of the repository should actually be aware that Book also extends Eloquent\Model and that it can call a save() method on it. The point is that it doesn't (=shouldn't) matter if Book extends Model or not, it should still be instantiable anywhere in your application, because within your business logic it's just a Book, i.e. a Plain Old PHP Object with some attributes and methods describing a book and not the strategies how to find or persist the object. That's what repositories are for.
But yes, the absolute clean way is to have a BookInterface and then bind it to a specific implementation. So it could all look like this:
Interfaces:
interface BookInterface
{
/**
* Get the ISBN.
*
* #return string
*/
public function getISBN();
}
interface BookRepositoryInterface()
{
/**
* Find a book by the given Id.
*
* #return null|BookInterface
*/
public function find($id);
}
Concrete implementations:
class Book extends Model implements BookInterface
{
public function getISBN()
{
return $this->isbn;
}
}
class EloquentBookRepository implements BookRepositoryInterface
{
protected $book;
public function __construct(Model $book)
{
$this->book = $book;
}
public function find($id)
{
return $this->book->find($id);
}
}
And then bind the interfaces to the desired implementations:
App::bind('BookInterface', function()
{
return new Book;
});
App::bind('BookRepositoryInterface', function()
{
return new EloquentBookRepository(new Book);
});
It doesn't matter if Book extends Model or anything else, as long as it implements the BookInterface, it is a Book. That's why I bravely instantiated a new Book in the test. Because it doesn't matter if you change the ORM, it only matters if you have several implementations of the BookInterface, but that's not very likely (sensible?), I guess. But just to play it safe, now that it's bound to the IoC-Container, you can instantiate it like this in the test:
$book = $this->app->make('BookInterface');
which will return an instance of whatever implementation of Book you're currently using.
So, for better testability
Code to interfaces rather than concrete classes
Use Laravel's IoC-Container to bind interfaces to concrete implementations (including mocks)
Use dependency injection
I hope that makes sense.
In Ember you can inject objects into views using an initializer as follows (Ember-CLI syntax):
export default {
name: 'sayHello',
initialize: function(container, app) {
var thing = function() {
return 'Hello';
};
app.register('sayHello:main', sayHello, { instantiate: false });
app.inject('view', 'sayHello', 'sayHello:main');
}
};
This injects the method into all views including link-tos, inputs, list items in a collection view, etc. This seems like it would hinder the app's performance if the thing being injected was substantially sized. In many situations, you just want to inject something into a route-specific view or controller. What I mean by that is a view that Ember automatically associates with the current route.
Question: Is there a way to inject objects into just the route-specific views and not into the link-tos, inputs, etc, and does injecting the methods into all views noticeably inhibit the application's performance?
The ember guide says:
Injections can also be made on a specific factory by using its full
name:
application.inject('route:index', 'logger', 'logger:main');
-- http://emberjs.com/guides/understanding-ember/dependency-injection-and-service-lookup/
So the question is – which factory do you want to target.
I'm not sure if there is a factory that includes all user-defined views, but excludes all framework views. You should probably be able to target single views with view:application, etc.
An option may be to have a proxy view, which extends Ember.View, and from which you extend all your app views.